Sfoglia il codice sorgente

feat: add endpoints for share tokens

KernelDeimos 11 mesi fa
parent
commit
301ffaf61d

+ 16 - 0
packages/backend/src/api/APIError.js

@@ -47,6 +47,10 @@ module.exports = class APIError {
                 `value of ${quot(key)} must be one of: ` +
                 allowed.map(v => quot(v)).join(', ')
         },
+        'invalid_token': {
+            status: 400,
+            message: () => 'Invalid token'
+        },
         // Things
         'disallowed_thing': {
             status: 400,
@@ -450,6 +454,18 @@ module.exports = class APIError {
                 `The value for ${quot(key)} has the following errors: ` +
                 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
         // TODO: specifying these errors here might be a violation

+ 5 - 3
packages/backend/src/routers/share.js

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

+ 135 - 0
packages/backend/src/services/ShareService.js

@@ -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 { Actor, UserActorType } = require("./auth/Actor");
 const BaseService = require("./BaseService");
@@ -7,12 +11,143 @@ class ShareService extends BaseService {
     static MODULES = {
         uuidv4: require('uuid').v4,
         validator: require('validator'),
+        express: require('express'),
     };
 
     async _init () {
         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 ({
         issuer,
         email,