feat: add endpoints for share tokens

This commit is contained in:
KernelDeimos 2024-06-20 21:53:55 -04:00 committed by Eric Dubé
parent db5990a989
commit 301ffaf61d
3 changed files with 156 additions and 3 deletions

View File

@ -47,6 +47,10 @@ module.exports = class APIError {
`value of ${quot(key)} must be one of: ` + `value of ${quot(key)} must be one of: ` +
allowed.map(v => quot(v)).join(', ') allowed.map(v => quot(v)).join(', ')
}, },
'invalid_token': {
status: 400,
message: () => 'Invalid token'
},
// Things // Things
'disallowed_thing': { 'disallowed_thing': {
status: 400, status: 400,
@ -450,6 +454,18 @@ module.exports = class APIError {
`The value for ${quot(key)} has the following errors: ` + `The value for ${quot(key)} has the following errors: ` +
errors.join('; ') errors.join('; ')
}, },
'share_expired': {
status: 422,
message: 'This share is expired.'
},
'email_must_be_confirmed': {
status: 422,
message: 'Email must be confirmed to apply a share.',
},
'can_not_apply_to_this_user': {
status: 422,
message: 'This share can not be applied to this user.',
},
// Chat // Chat
// TODO: specifying these errors here might be a violation // TODO: specifying these errors here might be a violation

View File

@ -484,13 +484,15 @@ const v0_2 = async (req, res) => {
}); });
return svc_token.sign('share', { return svc_token.sign('share', {
$: 'token:share', $: 'token:share',
$v: 'v0.0.0', $v: '0.0.0',
uid: share_uid, uid: share_uid,
}, {
expiresIn: '14d'
}); });
})(); })();
const email_link = config.origin + const email_link =
`/sharelink?token=${share_token}`; `${config.origin}?share_token=${share_token}`;
await svc_email.send_email({ email }, 'share_by_email', { await svc_email.send_email({ email }, 'share_by_email', {
link: email_link, link: email_link,

View File

@ -1,3 +1,7 @@
const APIError = require("../api/APIError");
const { get_user } = require("../helpers");
const configurable_auth = require("../middleware/configurable_auth");
const { Endpoint } = require("../util/expressutil");
const { whatis } = require("../util/langutil"); const { whatis } = require("../util/langutil");
const { Actor, UserActorType } = require("./auth/Actor"); const { Actor, UserActorType } = require("./auth/Actor");
const BaseService = require("./BaseService"); const BaseService = require("./BaseService");
@ -7,12 +11,143 @@ class ShareService extends BaseService {
static MODULES = { static MODULES = {
uuidv4: require('uuid').v4, uuidv4: require('uuid').v4,
validator: require('validator'), validator: require('validator'),
express: require('express'),
}; };
async _init () { async _init () {
this.db = await this.services.get('database').get(DB_WRITE, 'share'); this.db = await this.services.get('database').get(DB_WRITE, 'share');
} }
['__on_install.routes'] (_, { app }) {
// track: scoping iife
const router = (() => {
const require = this.require;
const express = require('express');
return express.Router();
})();
app.use('/sharelink', router);
const svc_share = this.services.get('share');
const svc_token = this.services.get('token');
Endpoint({
route: '/check',
methods: ['POST'],
handler: async (req, res) => {
// Potentially confusing:
// The "share token" and "share cookie token" are different!
// -> "share token" is from the email link;
// it has a longer expiry time and can be used again
// if the share session expires.
// -> "share cookie token" lets the backend know it
// should grant permissions when the correct user
// is logged in.
const share_token = req.body.token;
if ( ! share_token ) {
throw APIError.create('field_missing', null, {
key: 'token',
});
}
const decoded = await svc_token.verify('share', share_token);
console.log('decoded?', decoded);
if ( decoded.$ !== 'token:share' ) {
throw APIError.create('invalid_token');
}
const share = await svc_share.get_share({
uid: decoded.uid,
});
if ( ! share ) {
throw APIError.create('invalid_token');
}
res.json({
$: 'api:share',
uid: share.uid,
email: share.recipient_email,
});
},
}).attach(router);
Endpoint({
route: '/apply',
methods: ['POST'],
mw: [configurable_auth()],
handler: async (req, res) => {
const share_uid = req.body.uid;
const share = await svc_share.get_share({
uid: share_uid,
});
share.data = this.db.case({
mysql: () => share.data,
otherwise: () =>
JSON.parse(share.data ?? '{}'),
})();
if ( ! share ) {
throw APIError.create('share_expired');
}
const actor = Actor.adapt(req.actor ?? req.user);
if ( ! actor ) {
// this shouldn't happen; auth should catch it
throw new Error('actor missing');
}
if ( ! actor.type.user.email_confirmed ) {
throw APIError.create('email_must_be_confirmed');
}
if ( actor.type.user.email !== share.recipient_email ) {
throw APIError.create('can_not_apply_to_this_user');
}
const issuer_user = await get_user({
id: share.issuer_user_id,
});
if ( ! issuer_user ) {
throw APIError.create('share_expired');
}
const issuer_actor = await Actor.create(UserActorType, {
user: issuer_user,
});
const svc_permission = this.services.get('permission');
for ( const permission of share.data.permissions ) {
await svc_permission.grant_user_user_permission(
issuer_actor,
actor.type.user.username,
permission,
);
}
res.json({
$: 'api:status-report',
status: 'success',
});
}
}).attach(router);
}
async get_share ({ uid }) {
const [share] = await this.db.read(
'SELECT * FROM share WHERE uid = ?',
[uid],
);
return share;
}
async create_share ({ async create_share ({
issuer, issuer,
email, email,