浏览代码

feat: update share endpoint to support more things

KernelDeimos 11 月之前
父节点
当前提交
dd5fde5130

+ 2 - 1
doc/api/type-tagged.md

@@ -30,9 +30,10 @@ anywhere.
 ## Specification
 
 - The `"$"` key indicates a type (or class) of object
-- Any key beginning with `$` is a **meta-key**
+- Any other key beginning with `$` is a **meta-key**
 - Other keys are not allowed to contain `$`
 - `"$version"` must follow [semver](https://semver.org/)
+- Keys with multiple `"$"` symbols are reserved for future use
 
 ## Alternative Representations
 

+ 3 - 0
packages/backend/src/CoreModule.js

@@ -57,6 +57,9 @@ const install = async ({ services, app, useapi }) => {
     const ArrayUtil = require('./libraries/ArrayUtil');
     services.registerService('util-array', ArrayUtil);
     
+    const LibTypeTagged = require('./libraries/LibTypeTagged');
+    services.registerService('lib-type-tagged', LibTypeTagged);
+    
     // === SERVICES ===
 
     // /!\ IMPORTANT /!\

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

@@ -33,6 +33,29 @@ module.exports = class APIError {
             status: 500,
             message: () => `An unknown error occurred`,
         },
+        'format_error': {
+            status: 400,
+            message: ({ message }) => `format error: ${message}`,
+        },
+        'temp_error': {
+            status: 400,
+            message: ({ message }) => `error: ${message}`,
+        },
+        
+        // Things
+        'disallowed_thing': {
+            status: 400,
+            message: ({ thing_type, accepted }) =>
+                `Request contained a ${quot(thing_type)} in a ` +
+                `place where ${quot(thing_type)} isn't accepted` +
+                (
+                    accepted
+                        ? '; ' +
+                            'accepted types are: ' +
+                            accepted.map(v => quot(v)).join(', ')
+                        : ''
+                ) + '.'
+        },
         
         // Unorganized
         'item_with_same_name_exists': {

+ 88 - 0
packages/backend/src/libraries/LibTypeTagged.js

@@ -0,0 +1,88 @@
+const Library = require("../definitions/Library");
+const { whatis } = require("../util/langutil");
+
+class LibTypeTagged extends Library {
+    process (o) {
+        const could_be = whatis(o) === 'object' || Array.isArray(o);
+        if ( ! could_be ) return {
+            $: 'error',
+            code: 'invalid-type',
+            message: 'should be object or array',
+        };
+        
+        const intermediate = this.get_intermediate_(o);
+        
+        if ( ! intermediate.type ) return {
+            $: 'error',
+            code: 'missing-type-param',
+            message: 'type parameter is missing',
+        };
+        
+        return this.intermediate_to_standard_(intermediate);
+    }
+    
+    intermediate_to_standard_ (intermediate) {
+        const out = {};
+        out.$ = intermediate.type;
+        for ( const k in intermediate.meta ) {
+            out['$' + k] = intermediate.meta[k];
+        }
+        for ( const k in intermediate.body ) {
+            out[k] = intermediate.body[k];
+        }
+        return out;
+    }
+    
+    get_intermediate_ (o) {
+        if ( Array.isArray(o) ) {
+            return this.process_array_(o);
+        }
+        
+        if ( o['$'] === '$meta-body' ) {
+            return this.process_structured_(o);
+        }
+        
+        return this.process_standard_(o);
+    }
+    
+    process_array_ (a) {
+        if ( a.length <= 1 || a.length > 3 ) return {
+            $: 'error',
+            code: 'invalid-array-length',
+            message: 'tag-typed arrays should have 1-3 elements',
+        };
+        
+        const [type, body = {}, meta = {}] = a;
+        
+        return { $: '$', type, body, meta };
+    }
+    
+    process_structured_ (o) {
+        if ( ! o.hasOwnProperty('type') ) return {
+            $: 'error',
+            code: 'missing-type-property',
+            message: 'missing "type" property'
+        };
+        
+        return { $: '$', ...o };
+    }
+    
+    process_standard_ (o) {
+        const type = o.$;
+        const meta = {};
+        const body = {};
+        
+        for ( const k in o ) {
+            if ( k === '$' ) continue;
+            if ( k.startsWith('$') ) {
+                meta[k.slice(1)] = o[k];
+            } else {
+                body[k] = o[k];
+            }
+        }
+        
+        return { $: '$', type, meta, body };
+    }
+}
+
+module.exports = LibTypeTagged;

+ 90 - 58
packages/backend/src/routers/share.js

@@ -3,7 +3,7 @@ const { Endpoint } = require('../util/expressutil');
 
 const validator = require('validator');
 const APIError = require('../api/APIError');
-const { get_user } = require('../helpers');
+const { get_user, get_app } = require('../helpers');
 const { Context } = require('../util/context');
 const auth2 = require('../middleware/auth2');
 const config = require('../config');
@@ -140,6 +140,8 @@ const v0_2 = async (req, res) => {
     const svc_permission = req.services.get('permission');
     const svc_notification = req.services.get('notification');
 
+    const lib_typeTagged = req.services.get('lib-type-tagged');
+
     const actor = Context.get('actor');
     
     // === Request Validators ===
@@ -170,12 +172,12 @@ const v0_2 = async (req, res) => {
         return recipients;
     });
     
-    const validate_paths = UtilFn(paths => {
+    const validate_shares = UtilFn(shares => {
         // Single-values get adapted into an array
-        if ( ! Array.isArray(paths) ) {
-            paths = [paths];
+        if ( ! Array.isArray(shares) ) {
+            shares = [shares];
         }
-        return paths;
+        return shares;
     })
     
     // === Request Values ===
@@ -184,8 +186,8 @@ const v0_2 = async (req, res) => {
         validate_mode.if(req.body.mode) ?? false;
     const req_recipients =
         validate_recipients.if(req.body.recipients) ?? [];
-    const req_paths =
-        validate_paths.if(req.body.paths) ?? [];
+    const req_shares =
+        validate_shares.if(req.body.shares) ?? [];
         
     // === State Values ===
 
@@ -198,10 +200,10 @@ const v0_2 = async (req, res) => {
         // Results
         status: null,
         recipients: Array(req_recipients.length).fill(null),
-        paths: Array(req_paths.length).fill(null),
+        shares: Array(req_shares.length).fill(null),
     }
     const recipients_work = new WorkList();
-    const fsitems_work = new WorkList();
+    const shares_work = new WorkList();
     
     // const assert_work_item = (wut, item) => {
     //     if ( item.$ !== wut ) {
@@ -221,11 +223,11 @@ const v0_2 = async (req, res) => {
                 result.recipients[i] = result.recipients[i].serialize();
             }
         }
-        for ( let i=0 ; i < result.paths.length ; i++ ) {
-            if ( ! result.paths[i] ) continue;
-            if ( result.paths[i] instanceof APIError ) {
+        for ( let i=0 ; i < result.shares.length ; i++ ) {
+            if ( ! result.shares[i] ) continue;
+            if ( result.shares[i] instanceof APIError ) {
                 result.status = 'mixed';
-                result.paths[i] = result.paths[i].serialize();
+                result.shares[i] = result.shares[i].serialize();
             }
         }
     };
@@ -234,7 +236,7 @@ const v0_2 = async (req, res) => {
         console.log('OK');
         if (
             result.recipients.some(v => v !== null) ||
-            result.paths.some(v => v !== null)
+            result.shares.some(v => v !== null)
         ) {
             console.log('DOESNT THIS??')
             serialize_result();
@@ -329,74 +331,104 @@ const v0_2 = async (req, res) => {
     // --- Process Paths ---
     
     // Expect: at least one path
-    if ( req_paths.length < 1 ) {
+    if ( req_shares.length < 1 ) {
         throw APIError.create('field_invalid', null, {
-            key: 'paths',
+            key: 'shares',
             expected: 'at least one',
             got: 'none',
         })
     }
     
-    for ( let i=0 ; i < req_paths.length ; i++ ) {
-        const value = req_paths[i];
-        fsitems_work.push({ i, value });
+    for ( let i=0 ; i < req_shares.length ; i++ ) {
+        const value = req_shares[i];
+        shares_work.push({ i, value });
     }
-    fsitems_work.lockin();
+    shares_work.lockin();
     
-    for ( const item of fsitems_work.list() ) {
+    for ( const item of shares_work.list() ) {
          const { i } = item;
          let { value } = item;
         
-        // adapt all strings to objects
-        if ( typeof value === 'string' ) {
-            value = { path: value };
-        }
-        
-        if ( whatis(value) !== 'object' ) {
+        const thing = lib_typeTagged.process(value);
+        if ( thing.$ === 'error' ) {
             item.invalid = true;
-            result.paths[i] =
-                APIError.create('invalid_path', null, {
-                    path: item.path,
-                    value,
+            result.shares[i] =
+                APIError.create('format_error', null, {
+                    message: thing.message
                 });
             continue;
         }
         
-        const errors = [];
-        if ( ! value.path ) {
-            errors.push('`path` is required');
+        console.log('thing?', thing);
+        
+        const allowed_things = ['fs-share', 'app-share'];
+        if ( ! allowed_things.includes(thing.$) ) {
+            APIError.create('disallowed_thing', null, {
+                thing: thing.$,
+                accepted: allowed_things,
+            })
+        }
+        
+        if ( thing.$ === 'fs-share' ) {
+            item.type = 'fs';
+            const errors = [];
+            if ( ! thing.path ) {
+                errors.push('`path` is required');
+            }
+            let access = thing.access;
+            if ( access ) {
+                if ( ! ['read','write'].includes(access) ) {
+                    errors.push('`access` should be `read` or `write`');
+                }
+            } else access = 'read';
+
+            if ( errors.length ) {
+                item.invalid = true;
+                result.shares[item.i] =
+                    APIError.create('field_errors', null, {
+                        key: `shares[${item.i}]`,
+                        errors
+                    });
+                continue;
+            }
+            
+            item.path = thing.path;
+            item.permission = PermissionUtil.join('fs', thing.path, access);
         }
-        let access = value.access;
-        if ( access ) {
-            if ( ! ['read','write'].includes(access) ) {
-                errors.push('`access` should be `read` or `write`');
+        
+        if ( thing.$ === 'app-share' ) {
+            item.type = 'app';
+            const errors = [];
+            if ( ! thing.uid && thing.name ) {
+                errors.push('`uid` or `name` is required');
             }
-        } else access = 'read';
 
-        if ( errors.length ) {
-            item.invalid = true;
-            result.paths[item.i] =
-                APIError.create('field_errors', null, {
-                    path: item.path,
-                    errors
-                });
+            if ( errors.length ) {
+                item.invalid = true;
+                result.shares[item.i] =
+                    APIError.create('field_errors', null, {
+                        key: `shares[${item.i}]`,
+                        errors
+                    });
+                continue;
+            }
+            
+            item.permission = PermissionUtil.join('app', thing.path, 'access');
             continue;
         }
-        
-        item.path = value.path;
-        item.permission = PermissionUtil.join('fs', value.path, access);
     }
     
-    fsitems_work.clear_invalid();
+    shares_work.clear_invalid();
     
-    for ( const item of fsitems_work.list() ) {
+    for ( const item of shares_work.list() ) {
+        if ( item.type !== 'fs' ) continue;
         const node = await (new FSNodeParam('path')).consolidate({
             req, getParam: () => item.path
         });
         
         if ( ! await node.exists() ) {
             item.invalid = true;
-            result.paths[item.i] = APIError.create('subject_does_not_exist', {
+            result.shares[item.i] = APIError.create('subject_does_not_exist', {
                 path: item.path,
             })
             continue;
@@ -417,12 +449,12 @@ const v0_2 = async (req, res) => {
         item.email_link = email_link;
     }
     
-    fsitems_work.clear_invalid();
+    shares_work.clear_invalid();
 
     // Mark files as successful; further errors will be
     // reported on recipients instead.
-    for ( const item of fsitems_work.list() ) {
-        result.paths[item.i] =
+    for ( const item of shares_work.list() ) {
+        result.shares[item.i] =
             {
                 $: 'api:status-report',
                 status: 'success',
@@ -452,11 +484,11 @@ const v0_2 = async (req, res) => {
         
         const username = recipient_item.user.username;
 
-        for ( const path_item of fsitems_work.list() ) {
+        for ( const share_item of shares_work.list() ) {
             await svc_permission.grant_user_user_permission(
                 actor,
                 username,
-                path_item.permission,
+                share_item.permission,
             );
         }
         
@@ -478,7 +510,7 @@ const v0_2 = async (req, res) => {
         */
        
         const files = []; {
-            for ( const path_item of fsitems_work.list() ) {
+            for ( const path_item of shares_work.list() ) {
                 files.push(
                     await path_item.node.getSafeEntry(),
                 );