diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index e66f2dad..04718d63 100644 --- a/packages/backend/src/CoreModule.js +++ b/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 }) => { diff --git a/packages/backend/src/services/auth/TokenService.js b/packages/backend/src/services/auth/TokenService.js new file mode 100644 index 00000000..f71aff29 --- /dev/null +++ b/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 }; diff --git a/packages/backend/tools/test.js b/packages/backend/tools/test.js new file mode 100644 index 00000000..11a8ce43 --- /dev/null +++ b/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}`);