Forráskód Böngészése

Add TokenService and test utility

KernelDeimos 1 éve
szülő
commit
c1e4eeec32

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

@@ -198,6 +198,9 @@ const install = async ({ services, app }) => {
 
     const { Emailservice } = require('./services/EmailService');
     services.registerService('email', Emailservice);
+
+    const { TokenService } = require('./services/auth/TokenService');
+    services.registerService('token', TokenService);
 }
 
 const install_legacy = async ({ services }) => {

+ 172 - 0
packages/backend/src/services/auth/TokenService.js

@@ -0,0 +1,172 @@
+const BaseService = require("../BaseService");
+
+def = o => {
+    for ( let k in o ) {
+        if ( typeof o[k] === 'string' ) {
+            o[k] = { short: o[k] };
+        }
+    }
+    return {
+        fullkey_to_info: o,
+        short_to_fullkey: Object.keys(o).reduce((acc, key) => {
+            acc[o[key].short] = key;
+            return acc;
+        }, {}),
+    };
+}
+
+defv = o => {
+    return {
+        to_short: o,
+        to_long: Object.keys(o).reduce((acc, key) => {
+            acc[o[key]] = key;
+            return acc;
+        }, {}),
+    };
+};
+
+const compression = {
+    auth: def({
+        uuid: 'u',
+        type: {
+            short: 't',
+            values: defv({
+                'session': 's',
+                'access-token': 't',
+                'app-under-user': 'au',
+            }),
+        },
+        user_uid: 'uu',
+        app_uid: 'au',
+    }),
+};
+
+class TokenService extends BaseService {
+    static MODULES = {
+        jwt: require('jsonwebtoken'),
+    };
+
+    _construct () {
+        this.compression = compression;
+    }
+
+    _init () {
+        // TODO: move to service config
+        this.secret = this.global_config.jwt_secret;
+    }
+
+    sign (scope, payload, options) {
+        const require = this.require;
+
+        const jwt = require('jwt');
+        const secret = this.secret;
+
+        const context = this.compression[scope];
+        const compressed_payload = this._compress_payload(context, payload);
+
+        return jwt.sign(compressed_payload, secret, options);
+    }
+
+    verify (scope, token) {
+        const require = this.require;
+
+        const jwt = require('jwt');
+        const secret = this.secret;
+
+        const context = this.compression[scope];
+        const payload = jwt.verify(token, secret);
+
+        return this._decompress_payload(context, payload);
+    }
+
+    _compress_payload (context, payload) {
+        const fullkey_to_info = context.fullkey_to_info;
+
+        const compressed = {};
+
+        for ( let fullkey in payload ) {
+            if ( ! fullkey_to_info[fullkey] ) {
+                compressed[fullkey] = payload[fullkey];
+                continue;
+            }
+
+            let k = fullkey, v = payload[fullkey];
+            const compress_info = fullkey_to_info[fullkey];
+
+            if ( compress_info.short ) k = compress_info.short;
+            if ( compress_info.values && compress_info.values.to_short[v] ) {
+                v = compress_info.values.to_short[v];
+            }
+
+            compressed[k] = v;
+        }
+
+        return compressed;
+    }
+
+    _decompress_payload (context, payload) {
+        const fullkey_to_info = context.fullkey_to_info;
+        const short_to_fullkey = context.short_to_fullkey;
+
+        const decompressed = {};
+
+        for ( let short in payload ) {
+            if ( ! short_to_fullkey[short] ) {
+                decompressed[short] = payload[short];
+                continue;
+            }
+
+            let k = short, v = payload[short];
+            const fullkey = short_to_fullkey[short];
+            const compress_info = fullkey_to_info[fullkey];
+
+
+            if ( compress_info.short ) k = fullkey;
+            if ( compress_info.values && compress_info.values.to_long[v] ) {
+                v = compress_info.values.to_long[v];
+            }
+
+            decompressed[k] = v;
+        }
+
+        return decompressed;
+    }
+
+    _test ({ assert }) {
+        // Test compression
+        {
+            const context = this.compression.auth;
+            const payload = {
+                uuid: '123',
+                type: 'session',
+                user_uid: '456',
+                app_uid: '789',
+            };
+            
+            const compressed = this._compress_payload(context, payload);
+            assert(() => compressed.u === '123');
+            assert(() => compressed.t === 's');
+            assert(() => compressed.uu === '456');
+            assert(() => compressed.au === '789');
+        }
+
+        // Test decompression
+        {
+            const context = this.compression.auth;
+            const payload = {
+                u: '123',
+                t: 's',
+                uu: '456',
+                au: '789',
+            };
+            
+            const decompressed = this._decompress_payload(context, payload);
+            assert(() => decompressed.uuid === '123');
+            assert(() => decompressed.type === 'session');
+            assert(() => decompressed.user_uid === '456');
+            assert(() => decompressed.app_uid === '789');
+        }
+    }
+}
+
+module.exports = { TokenService };

+ 127 - 0
packages/backend/tools/test.js

@@ -0,0 +1,127 @@
+const { AdvancedBase } = require("@heyputer/puter-js-common");
+const CoreModule = require("../src/CoreModule");
+const { Context } = require("../src/util/context");
+
+class TestKernel extends AdvancedBase {
+    constructor () {
+        super();
+
+        this.modules = [];
+
+        this.logfn_ = (...a) => a;
+    }
+
+    add_module (module) {
+        this.modules.push(module);
+    }
+
+    boot () {
+        const { consoleLogManager } = require('../src/util/consolelog');
+        consoleLogManager.initialize_proxy_methods();
+
+        consoleLogManager.decorate_all(({ manager, replace }, ...a) => {
+            replace(...this.logfn_(...a));
+        });
+
+        const { Container } = require('../src/services/Container');
+
+        const services = new Container();
+        this.services = services;
+        // app.set('services', services);
+
+        const root_context = Context.create({
+            services,
+        }, 'app');
+        globalThis.root_context = root_context;
+
+        root_context.arun(async () => {
+            await this._install_modules();
+            // await this._boot_services();
+        });
+
+        // Error.stackTraceLimit = Infinity;
+        Error.stackTraceLimit = 200;
+    }
+
+    async _install_modules () {
+        const { services } = this;
+
+        for ( const module of this.modules ) {
+            await module.install(Context.get());
+        }
+
+        // Real kernel initializes services here, but in this test kernel
+        // we don't initialize any services.
+
+        // Real kernel adds legacy services here but these will break
+        // the test kernel.
+
+        services.ready.resolve();
+
+        // provide services to helpers
+        // const { tmp_provide_services } = require('../src/helpers');
+        // tmp_provide_services(services);
+    }
+}
+
+const k = new TestKernel();
+k.add_module(new CoreModule());
+k.boot();
+
+const do_after_tests_ = [];
+
+// const do_after_tests = (fn) => {
+//     do_after_tests_.push(fn);
+// };
+const repeat_after = (fn) => {
+    fn();
+    do_after_tests_.push(fn);
+};
+
+let total_passed = 0;
+let total_failed = 0;
+
+for ( const name in k.services.instances_ ) {
+    console.log('name', name)
+    const ins = k.services.instances_[name];
+    ins.construct();
+    if ( ! ins._test || typeof ins._test !== 'function' ) {
+        continue;
+    }
+    let passed = 0;
+    let failed = 0;
+
+    repeat_after(() => {
+        console.log(`\x1B[33;1m=== [ Service :: ${name} ] ===\x1B[0m`);
+    });
+
+    const testapi = {
+        assert: (condition, name) => {
+            name = name || condition.toString();
+            if ( condition() ) {
+                passed++;
+                repeat_after(() => console.log(`\x1B[32;1m  ✔ ${name}\x1B[0m`));
+            } else {
+                failed++;
+                repeat_after(() => console.log(`\x1B[31;1m  ✘ ${name}\x1B[0m`));
+            }
+        }
+    };
+
+    ins._test(testapi);
+
+    total_passed += passed;
+    total_failed += failed;
+}
+
+console.log(`\x1B[36;1m<===\x1B[0m ` +
+    'ASSERTION OUTPUTS ARE REPEATED BELOW' +
+    ` \x1B[36;1m===>\x1B[0m`);
+
+for ( const fn of do_after_tests_ ) {
+    fn();
+}
+
+console.log(`\x1B[36;1m=== [ Summary ] ===\x1B[0m`);
+console.log(`Passed: ${total_passed}`);
+console.log(`Failed: ${total_failed}`);