Skip to content

Commit

Permalink
feat: add share service and share-by-email to /share
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed Jun 21, 2024
1 parent fc4ae19 commit db5990a
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 13 deletions.
12 changes: 12 additions & 0 deletions doc/devmeta/track-comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Comments beginning with `// track:`. See

- `track: type check`:
A condition that's used to check the type of an imput.
- `track: adapt`
A value can by adapted from another type at this line.
- `track: bounds check`:
A condition that's used to check the bounds of an array
or other list-like entity.
Expand All @@ -22,3 +24,13 @@ Comments beginning with `// track:`. See
A common pattern where a prefix string is "sliced off"
of another string to obtain a significant value, such
as an indentifier.
- `track: actor type`
The sub-type of an Actor object is checked.
- `track: scoping iife`
An immediately-invoked function expression specifically
used to reduce scope clutter.
- `track: good candidate for sequence`
Some code involves a series of similar steps,
or there's a common behavior that should happen
in between. The Sequence class is good for this so
it might be a worthy migration.
3 changes: 3 additions & 0 deletions packages/backend/src/CoreModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ const install = async ({ services, app, useapi }) => {

const { ProtectedAppService } = require('./services/ProtectedAppService');
services.registerService('__protected-app', ProtectedAppService);

const { ShareService } = require('./services/ShareService');
services.registerService('share', ShareService);
}

const install_legacy = async ({ services }) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/backend/src/api/APIError.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ module.exports = class APIError {
status: 400,
message: ({ message }) => `error: ${message}`,
},

'disallowed_value': {
status: 400,
message: ({ key ,allowed }) =>
`value of ${quot(key)} must be one of: ` +
allowed.map(v => quot(v)).join(', ')
},
// Things
'disallowed_thing': {
status: 400,
Expand Down
97 changes: 87 additions & 10 deletions packages/backend/src/routers/share.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const v0_2 = async (req, res) => {
const svc_email = req.services.get('email');
const svc_permission = req.services.get('permission');
const svc_notification = req.services.get('notification');
const svc_share = req.services.get('share');

const lib_typeTagged = req.services.get('lib-type-tagged');

Expand Down Expand Up @@ -148,24 +149,27 @@ const v0_2 = async (req, res) => {
}
recipients_work.lockin();

// track: good candidate for sequence

// Expect: each value should be a valid username or email
for ( const item of recipients_work.list() ) {
const { value, i } = item;

if ( typeof value !== 'string' ) {
item.invalid = true;
result.recipients[i] =
APIError.create('invalid_username_of_email', null, {
APIError.create('invalid_username_or_email', null, {
value,
})
});
continue;
}

if ( value.match(config.username_regex) ) {
item.type = 'username';
continue;
}
if ( validator.isEmail(value) ) {
item.type = 'username';
item.type = 'email';
continue;
}

Expand All @@ -182,21 +186,58 @@ const v0_2 = async (req, res) => {
// Expect: no emails specified yet
// AND usernames exist
for ( const item of recipients_work.list() ) {
if ( item.type === 'email' ) {
const allowed_types = ['email', 'username'];
if ( ! allowed_types.includes(item.type) ) {
item.invalid = true;
result.recipients[item.i] =
APIError.create('future', null, {
what: 'specifying recipients by email',
value: item.value
APIError.create('disallowed_value', null, {
key: `recipients[${item.i}].type`,
allowed: allowed_types,
});
continue;
}
}

// Return: if there are invalid values in strict mode
recipients_work.clear_invalid();

for ( const item of recipients_work.list() ) {
if ( item.type !== 'email' ) continue;

const errors = [];
if ( ! validator.isEmail(item.value) ) {
errors.push('`email` is not valid');
}

if ( errors.length ) {
item.invalid = true;
result.recipients[item.i] =
APIError.create('field_errors', null, {
key: `recipients[${item.i}]`,
errors,
});
continue;
}
}

recipients_work.clear_invalid();

// CHECK EXISTING USERS FOR EMAIL SHARES
for ( const recipient_item of recipients_work.list() ) {
if ( recipient_item.type !== 'email' ) continue;
const user = await get_user({
email: recipient_item.value,
});
if ( ! user ) continue;
recipient_item.type = 'username';
recipient_item.value = user.username;
}

recipients_work.clear_invalid();

for ( const item of recipients_work.list() ) {
if ( item.type !== 'username' ) continue;

const user = await get_user({ username: item.value });
if ( ! user ) {
item.invalid = true;
Expand Down Expand Up @@ -243,8 +284,6 @@ const v0_2 = async (req, res) => {
continue;
}

console.log('thing?', thing);

const allowed_things = ['fs-share', 'app-share'];
if ( ! allowed_things.includes(thing.$) ) {
APIError.create('disallowed_thing', null, {
Expand Down Expand Up @@ -417,7 +456,45 @@ const v0_2 = async (req, res) => {
});

result.recipients[recipient_item.i] =
{ $: 'api:status-report', statis: 'success' };
{ $: 'api:status-report', status: 'success' };
}

for ( const recipient_item of recipients_work.list() ) {
if ( recipient_item.type !== 'email' ) continue;

const email = recipient_item.value;

// data that gets stored in the `data` column of the share
const data = {
$: 'internal:share',
$v: 'v0.0.0',
permissions: [],
};

for ( const share_item of shares_work.list() ) {
data.permissions.push(share_item.permission);
}

// track: scoping iife
const share_token = await (async () => {
const share_uid = await svc_share.create_share({
issuer: actor,
email,
data,
});
return svc_token.sign('share', {
$: 'token:share',
$v: 'v0.0.0',
uid: share_uid,
});
})();

const email_link = config.origin +
`/sharelink?token=${share_token}`;

await svc_email.send_email({ email }, 'share_by_email', {
link: email_link,
});
}

result.status = 'success';
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/routers/whoami.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const WHOAMI_GET = eggspress('/whoami', {
username: req.user.username,
uuid: req.user.uuid,
email: req.user.email,
unconfirmed_email: req.user.email,
email_confirmed: req.user.email_confirmed,
requires_email_confirmation: req.user.requires_email_confirmation,
desktop_bg_url: req.user.desktop_bg_url,
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/services/EmailService.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ If this was not you, please contact [email protected] immediately.
<p>Puter</p>
`
},
'share_by_email': {
subject: 'share by email',
html: `testing: {{link}}`
},
}

class Emailservice extends BaseService {
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/services/GetUserService.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class GetUserService extends BaseService {
async get_user (options) {
const user = await this.get_user_(options);
if ( ! user ) return null;

const svc_whoami = this.services.get('whoami');
await svc_whoami.get_details({ user }, user);
return user;
Expand Down
65 changes: 65 additions & 0 deletions packages/backend/src/services/ShareService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const { whatis } = require("../util/langutil");
const { Actor, UserActorType } = require("./auth/Actor");
const BaseService = require("./BaseService");
const { DB_WRITE } = require("./database/consts");

class ShareService extends BaseService {
static MODULES = {
uuidv4: require('uuid').v4,
validator: require('validator'),
};

async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'share');
}

async create_share ({
issuer,
email,
data,
}) {
const require = this.require;
const validator = require('validator');

// track: type check
if ( typeof email !== 'string' ) {
throw new Error('email must be a string');
}
// track: type check
if ( whatis(data) !== 'object' ) {
throw new Error('data must be an object');
}

// track: adapt
issuer = Actor.adapt(issuer);
// track: type check
if ( ! (issuer instanceof Actor) ) {
throw new Error('expected issuer to be Actor');
}

// track: actor type
if ( ! (issuer.type instanceof UserActorType) ) {
throw new Error('only users are allowed to create shares');
}

if ( ! validator.isEmail(email) ) {
throw new Error('invalid email');
}

const uuid = this.modules.uuidv4();

await this.db.write(
'INSERT INTO `share` ' +
'(`uid`, `issuer_user_id`, `recipient_email`, `data`) ' +
'VALUES (?, ?, ?, ?)',
[uuid, issuer.type.user.id, email, JSON.stringify(data)]
);

return uuid;
}
}

module.exports = {
ShareService,
};

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
this.db = new Database(this.config.path);

// Database upgrade logic
const TARGET_VERSION = 11;
const TARGET_VERSION = 12;

if ( do_setup ) {
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
Expand All @@ -60,6 +60,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
'0011_notification.sql',
'0012_appmetadata.sql',
'0013_protected-apps.sql',
'0014_share.sql',
].map(p => path_.join(__dirname, 'sqlite_setup', p));
const fs = require('fs');
for ( const filename of sql_files ) {
Expand Down Expand Up @@ -120,6 +121,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
upgrade_files.push('0013_protected-apps.sql');
}

if ( user_version <= 11 ) {
upgrade_files.push('0014_share.sql');
}

if ( upgrade_files.length > 0 ) {
this.log.noticeme(`Database out of date: ${this.config.path}`);
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/services/database/sqlite_setup/0014_share.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE `share` (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"uid" TEXT NOT NULL UNIQUE,
"issuer_user_id" INTEGER NOT NULL,
"recipient_email" TEXT NOT NULL,
"data" JSON DEFAULT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY ("issuer_user_id") REFERENCES "user" ("id")
ON DELETE CASCADE ON UPDATE CASCADE
);

0 comments on commit db5990a

Please sign in to comment.