From eac0c717b0a1c37ca3bc78c716b3296ca7995a16 Mon Sep 17 00:00:00 2001 From: ProgrammerIn-wonderland <3838shah@gmail.com> Date: Thu, 26 Mar 2026 03:40:16 -0400 Subject: [PATCH 1/3] Get rid of Endpoint and ExpectationService --- src/backend/src/ExtensionService.js | 16 +- .../src/filesystem/batch/BatchExecutor.js | 19 - .../src/modules/apps/AppIconService.js | 18 +- .../src/modules/broadcast/BroadcastService.js | 11 +- .../captcha/services/CaptchaService.js | 106 +++--- src/backend/src/modules/core/Core2Module.js | 3 - .../src/modules/core/ExpectationService.js | 134 ------- src/backend/src/modules/core/README.md | 24 -- src/backend/src/modules/core/lib/__lib__.js | 1 - src/backend/src/modules/core/lib/expect.js | 94 ----- .../development/LocalTerminalService.js | 154 ++++---- .../src/modules/template/TemplateService.js | 16 +- src/backend/src/services/ChatAPIService.js | 225 ++++++----- .../src/services/ChatAPIService.test.js | 75 ++-- src/backend/src/services/EntriService.js | 73 ++-- src/backend/src/services/KernelInfoService.js | 129 +++---- .../src/services/NotificationService.js | 72 ++-- src/backend/src/services/PeerService.js | 140 ++++--- .../src/services/PermissionAPIService.js | 300 ++++++++------- src/backend/src/services/PuterAPIService.js | 10 +- .../src/services/PuterHomepageService.js | 42 +-- src/backend/src/services/SNSService.js | 142 ++++--- src/backend/src/services/ShareService.js | 356 +++++++++--------- .../src/services/WebDAV/WebDAVService.js | 77 ++-- src/backend/src/services/WispService.js | 138 ++++--- src/backend/src/services/auth/ACLService.js | 122 +++--- src/backend/src/services/auth/AuthService.js | 80 ++-- .../web/UserProtectedEndpointsService.js | 16 +- .../src/services/worker/WorkerService.js | 61 +-- src/backend/src/util/expressutil.js | 14 +- 30 files changed, 1161 insertions(+), 1507 deletions(-) delete mode 100644 src/backend/src/modules/core/ExpectationService.js delete mode 100644 src/backend/src/modules/core/lib/expect.js diff --git a/src/backend/src/ExtensionService.js b/src/backend/src/ExtensionService.js index 7731b44d2d..ae8f132570 100644 --- a/src/backend/src/ExtensionService.js +++ b/src/backend/src/ExtensionService.js @@ -18,8 +18,8 @@ */ const { AdvancedBase } = require('@heyputer/putility'); +const eggspress = require('./api/eggspress'); const BaseService = require('./services/BaseService'); -const { Endpoint } = require('./util/expressutil'); const configurable_auth = require('./middleware/configurable_auth'); const { Context } = require('./util/context'); const { DB_WRITE } = require('./services/database/consts'); @@ -58,16 +58,14 @@ class ExtensionServiceState extends AdvancedBase { mw.push(configurable_auth(auth_conf)); } - const endpoint = Endpoint({ - methods: options.methods ?? ['GET'], + const router = eggspress(path, { + allowedMethods: options.methods ?? ['GET'], mw, - route: path, - handler: handler, ...(options.subdomain ? { subdomain: options.subdomain } : {}), otherOpts: options.otherOpts || {}, - }); + }, handler); - this.expressThings_.push({ type: 'endpoint', value: endpoint }); + this.expressThings_.push({ type: 'router', value: [router] }); } } @@ -181,10 +179,6 @@ class ExtensionService extends BaseService { '__on_install.routes' (_, { app }) { for ( const thing of this.state.expressThings_ ) { - if ( thing.type === 'endpoint' ) { - thing.value.attach(app); - continue; - } if ( thing.type === 'router' ) { app.use(...thing.value); continue; diff --git a/src/backend/src/filesystem/batch/BatchExecutor.js b/src/backend/src/filesystem/batch/BatchExecutor.js index 3a52c0566b..bedff71528 100644 --- a/src/backend/src/filesystem/batch/BatchExecutor.js +++ b/src/backend/src/filesystem/batch/BatchExecutor.js @@ -23,8 +23,6 @@ const APIError = require('../../api/APIError'); const { Context } = require('../../util/context'); const config = require('../../config'); const { TeePromise } = require('@heyputer/putility').libs.promise; -const { WorkUnit } = require('../../modules/core/lib/expect'); - class BatchExecutor extends AdvancedBase { static LOG_LEVEL = true; @@ -33,7 +31,6 @@ class BatchExecutor extends AdvancedBase { this.x = x; this.actor = actor; this.pathResolver = new PathResolver({ actor }); - this.expectations = x.get('services').get('expectations'); this.log = log; this.errors = errors; this.responsePromises = []; @@ -64,19 +61,12 @@ class BatchExecutor extends AdvancedBase { this.concurrent_ops++; - const { expectations } = this; const command_cls = commands[op.op]; if ( this.log_batchCommands ) { console.log(command_cls, JSON.stringify(op, null, 2)); } delete op.op; - const workUnit = WorkUnit.create(); - expectations.expect_eventually({ - workUnit, - checkpoint: 'operation responded', - }); - // TEMP: event service will handle this op.original_client_socket_id = req.body.original_client_socket_id; op.socket_id = req.body.socket_id; @@ -93,22 +83,13 @@ class BatchExecutor extends AdvancedBase { }); } - if ( file ) { - workUnit.checkpoint(`about to run << ${ - file.originalname ?? file.name - } >> ${ - JSON.stringify(op)}`); - } const command_ins = await command_cls.run({ getFile: () => file, pathResolver: this.pathResolver, actor: this.actor, }, op); - workUnit.checkpoint('operation invoked'); const res = await command_ins.awaitValue('result'); - // const res = await opctx.awaitValue('response'); - workUnit.checkpoint('operation responded'); return res; } catch (e) { this.hasError = true; diff --git a/src/backend/src/modules/apps/AppIconService.js b/src/backend/src/modules/apps/AppIconService.js index f51aea8daf..d92713ca73 100644 --- a/src/backend/src/modules/apps/AppIconService.js +++ b/src/backend/src/modules/apps/AppIconService.js @@ -27,7 +27,7 @@ import { NodePathSelector } from '../../filesystem/node/selectors.js'; import { get_app } from '../../helpers.js'; import BaseService from '../../services/BaseService.js'; import { DB_READ, DB_WRITE } from '../../services/database/consts.js'; -import { Endpoint } from '../../util/expressutil.js'; +import eggspress from '../../api/eggspress.js'; import { buffer_to_stream, stream_to_buffer } from '../../util/streamutil.js'; import { AppRedisCacheSpace } from './AppRedisCacheSpace.js'; import DEFAULT_APP_ICON from './default-app-icon.js'; @@ -106,16 +106,12 @@ export class AppIconService extends BaseService { stream.pipe(res); }; - Endpoint({ - route: '/app-icon/:app_uid', - methods: ['GET'], - handler, - }).attach(app); - Endpoint({ - route: '/app-icon/:app_uid/:size', - methods: ['GET'], - handler, - }).attach(app); + app.use(eggspress('/app-icon/:app_uid', { + allowedMethods: ['GET'], + }, handler)); + app.use(eggspress('/app-icon/:app_uid/:size', { + allowedMethods: ['GET'], + }, handler)); } getSizes () { diff --git a/src/backend/src/modules/broadcast/BroadcastService.js b/src/backend/src/modules/broadcast/BroadcastService.js index 11f03f3e03..c6a1e7f0e5 100644 --- a/src/backend/src/modules/broadcast/BroadcastService.js +++ b/src/backend/src/modules/broadcast/BroadcastService.js @@ -20,9 +20,9 @@ import { createHmac, randomUUID, timingSafeEqual } from 'crypto'; import { Agent as HttpsAgent } from 'https'; import axios from 'axios'; import { redisClient } from '../../clients/redis/redisSingleton.js'; +import eggspress from '../../api/eggspress.js'; import { BaseService } from '../../services/BaseService.js'; import { Context } from '../../util/context.js'; -import { Endpoint } from '../../util/expressutil.js'; export class BroadcastService extends BaseService { #peersByKey = {}; @@ -348,12 +348,9 @@ export class BroadcastService extends BaseService { const svc_web = this.services.get('web-server'); svc_web.allow_undefined_origin('/broadcast/webhook'); - // TODO DS: stop using Endpoint - Endpoint({ - route: '/broadcast/webhook', - methods: ['POST'], - handler: this.#handleWebhookRequest.bind(this), - }).attach(app); + app.use(eggspress('/broadcast/webhook', { + allowedMethods: ['POST'], + }, this.#handleWebhookRequest.bind(this))); } async #handleWebhookRequest (req, res) { diff --git a/src/backend/src/modules/captcha/services/CaptchaService.js b/src/backend/src/modules/captcha/services/CaptchaService.js index 74dccf40ba..d76eb1ec99 100644 --- a/src/backend/src/modules/captcha/services/CaptchaService.js +++ b/src/backend/src/modules/captcha/services/CaptchaService.js @@ -18,7 +18,7 @@ */ const BaseService = require('../../../services/BaseService'); -const { Endpoint } = require('../../../util/expressutil'); +const eggspress = require('../../../api/eggspress'); const { checkCaptcha } = require('../middleware/captcha-middleware'); /** @@ -129,36 +129,32 @@ class CaptchaService extends BaseService { app.use('/api/captcha', api); // Generate captcha endpoint - Endpoint({ - route: '/generate', - methods: ['GET'], - handler: async (req, res) => { - const captcha = this.generateCaptcha(); - res.json({ - token: captcha.token, - image: captcha.data, - }); - }, - }).attach(api); + api.use(eggspress('/generate', { + allowedMethods: ['GET'], + }, async (req, res) => { + const captcha = this.generateCaptcha(); + res.json({ + token: captcha.token, + image: captcha.data, + }); + })); // Verify captcha endpoint - Endpoint({ - route: '/verify', - methods: ['POST'], - handler: (req, res) => { - const { token, answer } = req.body; - - if ( !token || !answer ) { - return res.status(400).json({ - valid: false, - error: 'Missing token or answer', - }); - } + api.use(eggspress('/verify', { + allowedMethods: ['POST'], + }, (req, res) => { + const { token, answer } = req.body; + + if ( !token || !answer ) { + return res.status(400).json({ + valid: false, + error: 'Missing token or answer', + }); + } - const isValid = this.verifyCaptcha(token, answer); - res.json({ valid: isValid }); - }, - }).attach(api); + const isValid = this.verifyCaptcha(token, answer); + res.json({ valid: isValid }); + })); // Special endpoint for automated testing // This should be disabled in production @@ -430,8 +426,10 @@ class CaptchaService extends BaseService { // Invalid token or expired if ( ! captchaData ) { console.log('Verification FAILED: No data found for this token'); - console.log('TOKENS_TRACKING: Available tokens (first 8 chars):', - Array.from(this.captchaTokens.keys()).map(t => t.substring(0, 8))); + console.log( + 'TOKENS_TRACKING: Available tokens (first 8 chars):', + Array.from(this.captchaTokens.keys()).map(t => t.substring(0, 8)), + ); this.log.debug(`Invalid captcha token: ${token}`); return false; } @@ -542,29 +540,29 @@ class CaptchaService extends BaseService { }; switch ( this.difficulty ) { - case 'easy': - return { - ...baseOptions, - size: 4, - width: 150, - height: 50, - noise: 1, - }; - case 'hard': - return { - ...baseOptions, - size: 7, - width: 200, - height: 60, - noise: 3, - }; - case 'medium': - default: - return { - ...baseOptions, - width: 180, - height: 50, - }; + case 'easy': + return { + ...baseOptions, + size: 4, + width: 150, + height: 50, + noise: 1, + }; + case 'hard': + return { + ...baseOptions, + size: 7, + width: 200, + height: 60, + noise: 3, + }; + case 'medium': + default: + return { + ...baseOptions, + width: 180, + height: 50, + }; } } @@ -643,4 +641,4 @@ class CaptchaService extends BaseService { // Export both as a named export and as a default export for compatibility module.exports = CaptchaService; -module.exports.CaptchaService = CaptchaService; \ No newline at end of file +module.exports.CaptchaService = CaptchaService; diff --git a/src/backend/src/modules/core/Core2Module.js b/src/backend/src/modules/core/Core2Module.js index 7758753fc3..32efc29938 100644 --- a/src/backend/src/modules/core/Core2Module.js +++ b/src/backend/src/modules/core/Core2Module.js @@ -58,9 +58,6 @@ class Core2Module extends AdvancedBase { const { PagerService } = require('./PagerService.js'); services.registerService('pager', PagerService); - const { ExpectationService } = require('./ExpectationService.js'); - services.registerService('expectations', ExpectationService); - const { ProcessEventService } = require('./ProcessEventService.js'); services.registerService('process-event', ProcessEventService); diff --git a/src/backend/src/modules/core/ExpectationService.js b/src/backend/src/modules/core/ExpectationService.js deleted file mode 100644 index 8506ea4543..0000000000 --- a/src/backend/src/modules/core/ExpectationService.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -const { v4: uuidv4 } = require('uuid'); -const BaseService = require('../../services/BaseService'); - -/** -* @class ExpectationService -* @extends BaseService -* -* The `ExpectationService` is a specialized service designed to assist in the diagnosis and -* management of errors related to the intricate interactions among asynchronous operations. -* It facilitates tracking and reporting on expectations, enabling better fault isolation -* and resolution in systems where synchronization and timing of operations are crucial. -* -* This service inherits from the `BaseService` and provides methods for registering, -* purging, and handling expectations, making it a valuable tool for diagnosing complex -* runtime behaviors in a system. -*/ -class ExpectationService extends BaseService { - static USE = { - expect: 'core.expect', - }; - - /** - * Constructs the ExpectationService and initializes its internal state. - * This method is intended to be called asynchronously. - * It sets up the `expectations_` array which will be used to track expectations. - * - * @async - */ - async _construct () { - this.expectations_ = []; - } - - /** - * ExpectationService registers its commands at the consolidation phase because - * the '_init' method of CommandService may not have been called yet. - */ - '__on_boot.consolidation' () { - const commands = this.services.get('commands'); - commands.registerCommands('expectations', [ - { - id: 'pending', - description: 'lists pending expectations', - handler: async (args, log) => { - this.purgeExpectations_(); - if ( this.expectations_.length < 1 ) { - log.log('there are none'); - return; - } - for ( const expectation of this.expectations_ ) { - expectation.report(log); - } - }, - }, - ]); - } - - /** - * Initializes the ExpectationService, setting up interval functions and registering commands. - * - * This method sets up a periodic interval to purge expectations and registers a command - * to list pending expectations. The interval invokes `purgeExpectations_` every second. - * The command 'pending' allows users to list and log all pending expectations. - * - * @returns {Promise} A promise that resolves when initialization is complete. - */ - async _init () { - // TODO: service to track all interval functions? - /** - * Initializes the service by setting up interval functions and registering commands. - * This method sets up a periodic interval function to purge expectations and registers - * a command to list pending expectations. - * - * @returns {void} - */ - - // The comment should be placed above the method at line 68 - setInterval(() => { - this.purgeExpectations_(); - }, 1000); - } - - /** - * Purges expectations that have been met. - * - * This method iterates through the list of expectations and removes - * those that have been satisfied. Currently, this functionality is - * disabled and needs to be re-enabled. - * - * @returns {void} This method does not return anything. - */ - purgeExpectations_ () { - return; - // TODO: Re-enable this - // for ( let i=0 ; i < this.expectations_.length ; i++ ) { - // if ( this.expectations_[i].check() ) { - // this.expectations_[i] = null; - // } - // } - // this.expectations_ = this.expectations_.filter(v => v !== null); - } - - /** - * Registers an expectation to be tracked by the service. - * - * @param {Object} workUnit - The work unit to track - * @param {string} checkpoint - The checkpoint to expect - * @returns {void} - */ - expect_eventually ({ workUnit, checkpoint }) { - this.expectations_.push(new this.expect.CheckpointExpectation(workUnit, checkpoint)); - } -} - -module.exports = { - ExpectationService, -}; \ No newline at end of file diff --git a/src/backend/src/modules/core/README.md b/src/backend/src/modules/core/README.md index 2ea584b5e1..026ce5d69b 100644 --- a/src/backend/src/modules/core/README.md +++ b/src/backend/src/modules/core/README.md @@ -77,28 +77,6 @@ the source of the error. - **location:** The location where the error occurred. - **fields:** The error details to report. -### ExpectationService - - - -#### Listeners - -##### `boot.consolidation` - -ExpectationService registers its commands at the consolidation phase because -the '_init' method of CommandService may not have been called yet. - -#### Methods - -##### `expect_eventually` - -Registers an expectation to be tracked by the service. - -###### Parameters - -- **workUnit:** The work unit to track -- **checkpoint:** The checkpoint to expect - ### LogService The `LogService` class extends `BaseService` and is responsible for managing and @@ -183,8 +161,6 @@ through the logging and error reporting services. ## Libraries -### core.expect - ### core.util.identutil #### Functions diff --git a/src/backend/src/modules/core/lib/__lib__.js b/src/backend/src/modules/core/lib/__lib__.js index 05448a802c..bb4fb2e549 100644 --- a/src/backend/src/modules/core/lib/__lib__.js +++ b/src/backend/src/modules/core/lib/__lib__.js @@ -24,5 +24,4 @@ module.exports = { stdioutil: require('./stdio.js'), linuxutil: require('./linux.js'), }, - expect: require('./expect.js'), }; diff --git a/src/backend/src/modules/core/lib/expect.js b/src/backend/src/modules/core/lib/expect.js deleted file mode 100644 index 4992a20d59..0000000000 --- a/src/backend/src/modules/core/lib/expect.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -// METADATA // {"def":"core.expect"} -const { v4: uuidv4 } = require('uuid'); -const global_config = require('../../../config'); - -/** -* @class WorkUnit -* @description The WorkUnit class represents a unit of work that can be tracked and monitored for checkpoints. -* It includes methods to create instances, set checkpoints, and manage the state of the work unit. -*/ -class WorkUnit { - /** - * Represents a unit of work with checkpointing capabilities. - * - * @class - */ - - /** - * Creates and returns a new instance of WorkUnit. - * - * @static - * @returns {WorkUnit} A new instance of WorkUnit. - */ - static create () { - return new WorkUnit(); - } - /** - * Creates a new instance of the WorkUnit class. - * @static - * @returns {WorkUnit} A new WorkUnit instance. - */ - constructor () { - this.id = uuidv4(); - this.checkpoint_ = null; - } - checkpoint (label) { - if ( (global_config.logging ?? [] ).includes('checkpoint') ) { - console.log('CHECKPOINT', label); - } - this.checkpoint_ = label; - } -} - -/** -* @class CheckpointExpectation -* @classdesc The CheckpointExpectation class is used to represent an expectation that a specific checkpoint -* will be reached during the execution of a work unit. It includes methods to check if the checkpoint has -* been reached and to report the results of this check. -*/ -class CheckpointExpectation { - constructor (workUnit, checkpoint) { - this.workUnit = workUnit; - this.checkpoint = checkpoint; - } - /** - * Constructor for CheckpointExpectation class. - * Initializes the instance with a WorkUnit and a checkpoint label. - * @param {WorkUnit} workUnit - The work unit associated with the checkpoint. - * @param {string} checkpoint - The checkpoint label to be checked. - */ - check () { - // TODO: should be true if checkpoint was ever reached - return this.workUnit.checkpoint_ == this.checkpoint; - } - report (log) { - if ( this.check() ) return; - log.log(`operation(${this.workUnit.id}): ` + - `expected ${JSON.stringify(this.checkpoint)} ` + - `and got ${JSON.stringify(this.workUnit.checkpoint_)}.`); - } -} - -module.exports = { - WorkUnit, - CheckpointExpectation, -}; diff --git a/src/backend/src/modules/development/LocalTerminalService.js b/src/backend/src/modules/development/LocalTerminalService.js index e9c246bff5..bee9b048d9 100644 --- a/src/backend/src/modules/development/LocalTerminalService.js +++ b/src/backend/src/modules/development/LocalTerminalService.js @@ -19,8 +19,8 @@ const { spawn } = require('child_process'); const APIError = require('../../api/APIError'); +const eggspress = require('../../api/eggspress'); const configurable_auth = require('../../middleware/configurable_auth'); -const { Endpoint } = require('../../util/expressutil'); const PERM_LOCAL_TERMINAL = 'local-terminal:access'; @@ -36,9 +36,11 @@ class LocalTerminalService extends BaseService { get_profiles () { return { 'api-test': { - cwd: path_.join(__dirname, - '../../../../../', - 'tools/api-tester'), + cwd: path_.join( + __dirname, + '../../../../../', + 'tools/api-tester', + ), shell: [ '/usr/bin/env', 'node', 'apitest.js', @@ -56,91 +58,87 @@ class LocalTerminalService extends BaseService { })(); app.use('/local-terminal', r_group); - Endpoint({ - route: '/new', - methods: ['POST'], + r_group.use(eggspress('/new', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - const term_uuid = require('uuid').v4(); - - const svc_permission = this.services.get('permission'); - const actor = Context.get('actor'); - const can_access = actor && - await svc_permission.check(actor, PERM_LOCAL_TERMINAL); - - if ( ! can_access ) { - throw APIError.create('permission_denied', null, { - permission: PERM_LOCAL_TERMINAL, - }); - } + }, async (req, res) => { + const term_uuid = require('uuid').v4(); - const profiles = this.get_profiles(); - if ( ! profiles[req.body.profile] ) { - throw APIError.create('invalid_profile', null, { - profile: req.body.profile, - }); - } + const svc_permission = this.services.get('permission'); + const actor = Context.get('actor'); + const can_access = actor && + await svc_permission.check(actor, PERM_LOCAL_TERMINAL); - const profile = profiles[req.body.profile]; + if ( ! can_access ) { + throw APIError.create('permission_denied', null, { + permission: PERM_LOCAL_TERMINAL, + }); + } - const args = profile.shell.slice(1); - if ( profile.allow_args && req.body.args ) { - args.push(...req.body.args); - } - const proc = spawn(profile.shell[0], args, { - shell: true, - env: { - ...process.env, - ...(profile.env ?? {}), - }, - cwd: profile.cwd, + const profiles = this.get_profiles(); + if ( ! profiles[req.body.profile] ) { + throw APIError.create('invalid_profile', null, { + profile: req.body.profile, }); + } - // stdout to websocket - { - const svc_socketio = req.services.get('socketio'); - proc.stdout.on('data', data => { - const base64 = data.toString('base64'); - console.debug('---------------------- CHUNK?', base64); - svc_socketio.send({ room: req.user.id }, - 'local-terminal.stdout', - { - term_uuid, - base64, - }); - }); - proc.stderr.on('data', data => { - const base64 = data.toString('base64'); - console.debug('---------------------- CHUNK?', base64); - svc_socketio.send({ room: req.user.id }, - 'local-terminal.stderr', - { - term_uuid, - base64, - }); - }); - } + const profile = profiles[req.body.profile]; - proc.on('exit', () => { - this.log.noticeme(`[${term_uuid}] Process exited (${proc.exitCode})`); - delete this.sessions_[term_uuid]; + const args = profile.shell.slice(1); + if ( profile.allow_args && req.body.args ) { + args.push(...req.body.args); + } + const proc = spawn(profile.shell[0], args, { + shell: true, + env: { + ...process.env, + ...(profile.env ?? {}), + }, + cwd: profile.cwd, + }); - const svc_socketio = req.services.get('socketio'); - svc_socketio.send({ room: req.user.id }, - 'local-terminal.exit', - { - term_uuid, - }); + // stdout to websocket + { + const svc_socketio = req.services.get('socketio'); + proc.stdout.on('data', data => { + const base64 = data.toString('base64'); + console.debug('---------------------- CHUNK?', base64); + svc_socketio.send( + { room: req.user.id }, + 'local-terminal.stdout', + { term_uuid, base64 }, + ); }); + proc.stderr.on('data', data => { + const base64 = data.toString('base64'); + console.debug('---------------------- CHUNK?', base64); + svc_socketio.send( + { room: req.user.id }, + 'local-terminal.stderr', + { term_uuid, base64 }, + ); + }); + } - this.sessions_[term_uuid] = { - uuid: term_uuid, - proc, - }; + proc.on('exit', () => { + this.log.noticeme(`[${term_uuid}] Process exited (${proc.exitCode})`); + delete this.sessions_[term_uuid]; - res.json({ term_uuid }); - }, - }).attach(r_group); + const svc_socketio = req.services.get('socketio'); + svc_socketio.send( + { room: req.user.id }, + 'local-terminal.exit', + { term_uuid }, + ); + }); + + this.sessions_[term_uuid] = { + uuid: term_uuid, + proc, + }; + + res.json({ term_uuid }); + })); } async _init () { const svc_event = this.services.get('event'); diff --git a/src/backend/src/modules/template/TemplateService.js b/src/backend/src/modules/template/TemplateService.js index 520f7f72ec..cc51b86d96 100644 --- a/src/backend/src/modules/template/TemplateService.js +++ b/src/backend/src/modules/template/TemplateService.js @@ -19,7 +19,7 @@ // TODO: import via `USE` static member const BaseService = require('../../services/BaseService'); -const { Endpoint } = require('../../util/expressutil'); +const eggspress = require('../../api/eggspress'); /** * This is a template service that you can copy and paste to create new services. @@ -47,15 +47,11 @@ class TemplateService extends BaseService { */ '__on_install.routes' (_, { app }) { this.log.info('TemplateService get the event for installing endpoint.'); - Endpoint({ - route: '/example-endpoint', - methods: ['GET'], - handler: async (req, res) => { - res.send(this.workinprogress.hello_world()); - }, - }).attach(app); - // ^ Don't forget to attach the endpoint to the app! - // it's very easy to forget this step. + app.use(eggspress('/example-endpoint', { + allowedMethods: ['GET'], + }, async (req, res) => { + res.send(this.workinprogress.hello_world()); + })); } /** diff --git a/src/backend/src/services/ChatAPIService.js b/src/backend/src/services/ChatAPIService.js index dc53fd747f..50947db4c6 100644 --- a/src/backend/src/services/ChatAPIService.js +++ b/src/backend/src/services/ChatAPIService.js @@ -16,8 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - -const { Endpoint } = require('../util/expressutil'); +const eggspress = require('../api/eggspress'); const BaseService = require('./BaseService'); const APIError = require('../api/APIError'); @@ -30,7 +29,6 @@ const APIError = require('../api/APIError'); class ChatAPIService extends BaseService { static MODULES = { express: require('express'), - Endpoint: Endpoint, }; /** @@ -40,7 +38,7 @@ class ChatAPIService extends BaseService { * @param {Express} options.app Express application instance to install routes on * @returns {Promise} */ - async '__on_install.routes'(_, { app }) { + async '__on_install.routes' (_, { app }) { // Create a router for chat API endpoints const router = (() => { const require = this.require; @@ -61,135 +59,122 @@ class ChatAPIService extends BaseService { * @param {express.Router} options.router Express router to install endpoints on * @private */ - install_chat_endpoints_({ router }) { - const Endpoint = this.require('Endpoint'); + install_chat_endpoints_ ({ router }) { router.use(require('../routers/puterai/openai/completions')); router.use(require('../routers/puterai/openai/chat_completions')); router.use(require('../routers/puterai/openai/responses')); router.use(require('../routers/puterai/anthropic/messages')); // Endpoint to list available AI chat models - Endpoint({ - route: '/chat/models', - methods: ['GET'], - handler: async (req, res) => { - try { - // Use SUService to access AIChatService as system user - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_aiChat = this.services.get('ai-chat'); - // Return the simple model list which contains basic model information - return svc_aiChat.list(); - }); + router.use(eggspress('/chat/models', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + // Use SUService to access AIChatService as system user + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_aiChat = this.services.get('ai-chat'); + // Return the simple model list which contains basic model information + return svc_aiChat.list(); + }); - // Return the list of models - res.json({ models: models.filter(e => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e)) }); - } catch (error) { - this.log.error('Error fetching models:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + // Return the list of models + res.json({ models: models.filter(e => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e)) }); + } catch ( error ) { + this.log.error('Error fetching models:', error); + throw APIError.create('internal_server_error'); + } + })); // Endpoint to get detailed information about available AI chat models - Endpoint({ - route: '/chat/models/details', - methods: ['GET'], - handler: async (req, res) => { - try { - // Use SUService to access AIChatService as system user - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_aiChat = this.services.get('ai-chat'); - // Return the detailed model list which includes cost and capability information - return svc_aiChat.models(); - }); + router.use(eggspress('/chat/models/details', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + // Use SUService to access AIChatService as system user + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_aiChat = this.services.get('ai-chat'); + // Return the detailed model list which includes cost and capability information + return svc_aiChat.models(); + }); - // Return the detailed list of models - res.json({ models: models.filter((e) => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e.id)) }); - } catch (error) { - this.log.error('Error fetching model details:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + // Return the detailed list of models + res.json({ models: models.filter(e => !['costly', 'fake', 'abuse', 'model-fallback-test-1'].includes(e.id)) }); + } catch ( error ) { + this.log.error('Error fetching model details:', error); + throw APIError.create('internal_server_error'); + } + })); - Endpoint({ - route: '/image/models', - methods: ['GET'], - handler: async (req, res) => { - try { - // Use SUService to access AIImageGenerationService as system user - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_imageGen = this.services.get('ai-image'); - // Return the simple model list which contains basic model information - return svc_imageGen.list(); - }); - // Return the list of models - res.json({ models }); - } catch (error) { - this.log.error('Error fetching image models:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + router.use(eggspress('/image/models', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + // Use SUService to access AIImageGenerationService as system user + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_imageGen = this.services.get('ai-image'); + // Return the simple model list which contains basic model information + return svc_imageGen.list(); + }); + // Return the list of models + res.json({ models }); + } catch ( error ) { + this.log.error('Error fetching image models:', error); + throw APIError.create('internal_server_error'); + } + })); - Endpoint({ - route: '/image/models/details', - methods: ['GET'], - handler: async (req, res) => { - try { - // Use SUService to access AIImageGenerationService as system user - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_imageGen = this.services.get('ai-image'); - // Return the detailed model list which includes cost and capability information - return svc_imageGen.models(); - }); - // Return the detailed list of models - res.json({ models }); - } catch (error) { - this.log.error('Error fetching image model details:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + router.use(eggspress('/image/models/details', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + // Use SUService to access AIImageGenerationService as system user + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_imageGen = this.services.get('ai-image'); + // Return the detailed model list which includes cost and capability information + return svc_imageGen.models(); + }); + // Return the detailed list of models + res.json({ models }); + } catch ( error ) { + this.log.error('Error fetching image model details:', error); + throw APIError.create('internal_server_error'); + } + })); - Endpoint({ - route: '/video/models/details', - methods: ['GET'], - handler: async (req, res) => { - try { - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_video = this.services.get('ai-video'); - return svc_video.models(); - }); - res.json({ models }); - } catch (error) { - this.log.error('Error fetching video model details:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + router.use(eggspress('/video/models/details', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_video = this.services.get('ai-video'); + return svc_video.models(); + }); + res.json({ models }); + } catch ( error ) { + this.log.error('Error fetching video model details:', error); + throw APIError.create('internal_server_error'); + } + })); - Endpoint({ - route: '/video/models', - methods: ['GET'], - handler: async (req, res) => { - try { - const svc_su = this.services.get('su'); - const models = await svc_su.sudo(async () => { - const svc_video = this.services.get('ai-video'); - return svc_video.list(); - }); - res.json({ models }); - } catch (error) { - this.log.error('Error fetching video models:', error); - throw APIError.create('internal_server_error'); - } - }, - }).attach(router); + router.use(eggspress('/video/models', { + allowedMethods: ['GET'], + }, async (req, res) => { + try { + const svc_su = this.services.get('su'); + const models = await svc_su.sudo(async () => { + const svc_video = this.services.get('ai-video'); + return svc_video.list(); + }); + res.json({ models }); + } catch ( error ) { + this.log.error('Error fetching video models:', error); + throw APIError.create('internal_server_error'); + } + })); } } diff --git a/src/backend/src/services/ChatAPIService.test.js b/src/backend/src/services/ChatAPIService.test.js index 9fa2edb8ac..2c71c2de7b 100644 --- a/src/backend/src/services/ChatAPIService.test.js +++ b/src/backend/src/services/ChatAPIService.test.js @@ -42,13 +42,12 @@ describe('ChatAPIService', () => { let chatApiService; let mockServices; let mockRouter; - let mockApp; let mockSUService; let mockAIChatService; - let mockEndpoint; let mockWebServer; let mockReq; let mockRes; + let currentContext; beforeEach(() => { // Mock AIChatService @@ -91,19 +90,11 @@ describe('ChatAPIService', () => { get: vi.fn(), post: vi.fn(), }; - mockApp = { - use: vi.fn(), - }; - - // Mock Endpoint function - mockEndpoint = vi.fn().mockReturnValue({ - attach: vi.fn(), - }); - // Mock request and response mockReq = {}; mockRes = { json: vi.fn(), + locals: {}, }; // Setup ChatAPIService @@ -111,13 +102,14 @@ describe('ChatAPIService', () => { global_config: {}, config: {}, }); - chatApiService.modules.Endpoint = mockEndpoint; chatApiService.services = mockServices; chatApiService.log = { error: vi.fn(), }; Context.root.set('services', mockServices); + currentContext = Context.get(undefined, { allow_fallback: true }); + mockRes.locals.ctx = currentContext; // Mock the require function const oldInstanceRequire_ = chatApiService.require; @@ -127,54 +119,41 @@ describe('ChatAPIService', () => { }); }); + const getMountedRouteLayer = (router, path) => { + const mountedRouters = router.use.mock.calls.map(([mounted]) => mounted); + const mountedRouter = mountedRouters.find(candidate => + candidate?.stack?.some(layer => layer.route?.path === path)); + expect(mountedRouter).toBeTruthy(); + return mountedRouter.stack.find(layer => layer.route?.path === path); + }; + describe('install_chat_endpoints_', () => { it('should attach models endpoint to router', () => { - // Execute chatApiService.install_chat_endpoints_({ router: mockRouter }); - // Verify - expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({ - route: '/chat/models', - methods: ['GET'], - })); - expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({ - route: '/image/models', - methods: ['GET'], - })); + expect(getMountedRouteLayer(mockRouter, '/chat/models').route.methods.get).toBe(true); + expect(getMountedRouteLayer(mockRouter, '/image/models').route.methods.get).toBe(true); }); it('should attach models/details endpoint to router', () => { - // Setup - global.Endpoint = mockEndpoint; - - // Execute chatApiService.install_chat_endpoints_({ router: mockRouter }); - // Verify - expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({ - route: '/chat/models/details', - methods: ['GET'], - })); - expect(mockEndpoint).toHaveBeenCalledWith(expect.objectContaining({ - route: '/image/models/details', - methods: ['GET'], - })); + expect(getMountedRouteLayer(mockRouter, '/chat/models/details').route.methods.get).toBe(true); + expect(getMountedRouteLayer(mockRouter, '/image/models/details').route.methods.get).toBe(true); }); }); describe('/models endpoint', () => { it('should return list of models', async () => { - // Setup - global.Endpoint = mockEndpoint; chatApiService.install_chat_endpoints_({ router: mockRouter }); - // Get the handler function - const handler = mockEndpoint.mock.calls[0][0].handler; + const layer = getMountedRouteLayer(mockRouter, '/chat/models'); + const handler = layer.route.stack.at(-1).handle; - // Execute - await handler(mockReq, mockRes); + await currentContext.arun(async () => { + await handler(mockReq, mockRes, vi.fn()); + }); - // Verify expect(mockSUService.sudo).toHaveBeenCalled(); expect(mockRes.json).toHaveBeenCalledWith({ models: mockAIChatService.list(), @@ -184,17 +163,15 @@ describe('ChatAPIService', () => { describe('/models/details endpoint', () => { it('should return detailed list of models', async () => { - // Setup - global.Endpoint = mockEndpoint; chatApiService.install_chat_endpoints_({ router: mockRouter }); - // Get the handler function - const handler = mockEndpoint.mock.calls[1][0].handler; + const layer = getMountedRouteLayer(mockRouter, '/chat/models/details'); + const handler = layer.route.stack.at(-1).handle; - // Execute - await handler(mockReq, mockRes); + await currentContext.arun(async () => { + await handler(mockReq, mockRes, vi.fn()); + }); - // Verify expect(mockSUService.sudo).toHaveBeenCalled(); expect(mockRes.json).toHaveBeenCalledWith({ models: mockAIChatService.models(), diff --git a/src/backend/src/services/EntriService.js b/src/backend/src/services/EntriService.js index 784157bdf6..953ecd853c 100644 --- a/src/backend/src/services/EntriService.js +++ b/src/backend/src/services/EntriService.js @@ -18,15 +18,12 @@ */ const BaseService = require('./BaseService'); -const fs = require('node:fs'); const { Entity } = require('../om/entitystorage/Entity');; +const eggspress = require('../api/eggspress'); // const { get_app, subdomain } = require("../helpers"); let parseDomain ; const { Eq } = require('../om/query/query'); -const { Endpoint } = require('../util/expressutil'); -const { IncomingMessage } = require('node:http'); -const { Context } = require('../util/context'); const { createHash } = require('crypto'); const { NULL } = require('../om/proptypes/__all__'); const APIError = require('../api/APIError'); @@ -46,48 +43,46 @@ class EntriService extends BaseService { } '__on_install.routes' (_, { app }) { - Endpoint({ - route: '/entri/webhook', - methods: ['POST', 'GET'], + app.use(eggspress('/entri/webhook', { + allowedMethods: ['POST', 'GET'], /** * * @param {IncomingMessage} req * @param {*} res */ - handler: async (req, res) => { - if ( createHash('sha256').update(req.body.id + this.config.secret).digest('hex') !== req.headers['entri-signature'] ) { - res.status(401).send('Lol'); - return; - } - if ( ! req.body.data.records_propagated ) { - return; - } - let rootDomain = false; - if ( req.body.data.records_propagated[0].type === 'A' ) { - rootDomain = true; - } - - let realDomain = (rootDomain ? '' : (`${req.body.subdomain }.`)) + req.body.domain; - const svc_su = this.services.get('su'); - - const es_subdomain = this.services.get('es:subdomain'); - - await svc_su.sudo(async () => { - const rows = (await es_subdomain.select({ predicate: new Eq({ key: 'domain', value: `in-progress:${ realDomain}` }) })); - for ( const row of rows ) { - const entity = await Entity.create({ om: es_subdomain.om }, { - uid: row.values_.uid, - domain: realDomain, - }); - await es_subdomain.upsert(entity); + }, async (req, res) => { + if ( createHash('sha256').update(req.body.id + this.config.secret).digest('hex') !== req.headers['entri-signature'] ) { + res.status(401).send('Lol'); + return; + } + if ( ! req.body.data.records_propagated ) { + return; + } + let rootDomain = false; + if ( req.body.data.records_propagated[0].type === 'A' ) { + rootDomain = true; + } + + let realDomain = (rootDomain ? '' : (`${req.body.subdomain }.`)) + req.body.domain; + const svc_su = this.services.get('su'); + + const es_subdomain = this.services.get('es:subdomain'); + + await svc_su.sudo(async () => { + const rows = (await es_subdomain.select({ predicate: new Eq({ key: 'domain', value: `in-progress:${ realDomain}` }) })); + for ( const row of rows ) { + const entity = await Entity.create({ om: es_subdomain.om }, { + uid: row.values_.uid, + domain: realDomain, + }); + await es_subdomain.upsert(entity); - } - return true; - }); + } + return true; + }); - res.end('ok'); - }, - }).attach(app); + res.end('ok'); + })); const svc_web = this.services.get('web-server'); svc_web.allow_undefined_origin('/entri/webhook', '/entri/webhook'); diff --git a/src/backend/src/services/KernelInfoService.js b/src/backend/src/services/KernelInfoService.js index 08c2a7d877..9f8007f1c8 100644 --- a/src/backend/src/services/KernelInfoService.js +++ b/src/backend/src/services/KernelInfoService.js @@ -18,8 +18,8 @@ */ const configurable_auth = require('../middleware/configurable_auth'); +const eggspress = require('../api/eggspress'); const { Context } = require('../util/context'); -const { Endpoint } = require('../util/expressutil'); const BaseService = require('./BaseService'); const { Interface } = require('./drivers/meta/Construct'); @@ -55,88 +55,83 @@ class KernelInfoService extends BaseService { app.use('/', router); - Endpoint({ - route: '/lsmod', - methods: ['GET', 'POST'], + router.use(eggspress('/lsmod', { + allowedMethods: ['GET', 'POST'], mw: [ configurable_auth(), ], - handler: async (req, res) => { - const svc_permission = this.services.get('permission'); + }, async (req, res) => { + const svc_permission = this.services.get('permission'); - const actor = Context.get('actor'); - const can_see_all = actor && - await svc_permission.check(actor, PERM_SEE_ALL); - const can_see_drivers = actor && - await svc_permission.check(actor, PERM_SEE_DRIVERS); + const actor = Context.get('actor'); + const can_see_all = actor && + await svc_permission.check(actor, PERM_SEE_ALL); + const can_see_drivers = actor && + await svc_permission.check(actor, PERM_SEE_DRIVERS); - const interfaces = {}; - const svc_registry = this.services.get('registry'); - const col_interfaces = svc_registry.get('interfaces'); - for ( const interface_name of col_interfaces.keys() ) { - const iface = col_interfaces.get(interface_name); - if ( iface === undefined ) continue; - if ( iface.no_sdk ) continue; - interfaces[interface_name] = { - spec: (new Interface(iface, - { name: interface_name })).serialize(), - implementors: {}, - }; - } + const interfaces = {}; + const svc_registry = this.services.get('registry'); + const col_interfaces = svc_registry.get('interfaces'); + for ( const interface_name of col_interfaces.keys() ) { + const iface = col_interfaces.get(interface_name); + if ( iface === undefined ) continue; + if ( iface.no_sdk ) continue; + interfaces[interface_name] = { + spec: (new Interface( + iface, + { name: interface_name }, + )).serialize(), + implementors: {}, + }; + } - const services = []; - for ( const k in this.services.modules_ ) { - const module_info = { - name: k, - services: [], + const services = []; + for ( const k in this.services.modules_ ) { + for ( const s_k of this.services.modules_[k].services_l ) { + const service_info = { + name: s_k, + traits: [], }; + services.push(service_info); - for ( const s_k of this.services.modules_[k].services_l ) { - const service_info = { - name: s_k, - traits: [], - }; - services.push(service_info); - - const service = this.services.get(s_k); - if ( service.list_traits ) { - const traits = service.list_traits(); - for ( const trait of traits ) { - const corresponding_iface = interfaces[trait]; - if ( ! corresponding_iface ) continue; - corresponding_iface.implementors[s_k] = {}; - } - service_info.traits = service.list_traits(); + const service = this.services.get(s_k); + if ( service.list_traits ) { + const traits = service.list_traits(); + for ( const trait of traits ) { + const corresponding_iface = interfaces[trait]; + if ( ! corresponding_iface ) continue; + corresponding_iface.implementors[s_k] = {}; } + service_info.traits = service.list_traits(); } } + } - // If actor doesn't have permission to see all drivers, - // (granted by either "can_see_all" or "can_see_drivers") - if ( !can_see_all && !can_see_drivers ) { - // only show interfaces with at least one implementation - // that the actor has permission to use - for ( const iface_name in interfaces ) { - for ( const impl_name in interfaces[iface_name].implementors ) { - const perm = `service:${impl_name}:ii:${iface_name}`; - const can_see_this = actor && - await svc_permission.check(actor, perm); - if ( ! can_see_this ) { - delete interfaces[iface_name].implementors[impl_name]; - } - } - if ( Object.keys(interfaces[iface_name].implementors).length < 1 ) { - delete interfaces[iface_name]; + // If actor doesn't have permission to see all drivers, + // (granted by either "can_see_all" or "can_see_drivers") + if ( !can_see_all && !can_see_drivers ) { + // only show interfaces with at least one implementation + // that the actor has permission to use + for ( const iface_name in interfaces ) { + for ( const impl_name in interfaces[iface_name].implementors ) { + const perm = `service:${impl_name}:ii:${iface_name}`; + const can_see_this = actor && + await svc_permission.check(actor, perm); + if ( ! can_see_this ) { + delete interfaces[iface_name].implementors[impl_name]; } } + if ( Object.keys(interfaces[iface_name].implementors).length < 1 ) { + delete interfaces[iface_name]; + } } + } - res.json({ - interfaces, - ...(can_see_all ? { services } : {}), - }); - }, - }).attach(router); + res.json({ + interfaces, + ...(can_see_all ? { services } : {}), + }); + })); } } diff --git a/src/backend/src/services/NotificationService.js b/src/backend/src/services/NotificationService.js index e672e8c470..dd6a837ea7 100644 --- a/src/backend/src/services/NotificationService.js +++ b/src/backend/src/services/NotificationService.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ const APIError = require('../api/APIError'); +const eggspress = require('../api/eggspress'); const auth2 = require('../middleware/auth2'); -const { Endpoint } = require('../util/expressutil'); const { TeePromise } = require('@heyputer/putility').libs.promise; const BaseService = require('./BaseService'); const { DB_WRITE } = require('./database/consts'); @@ -125,35 +125,35 @@ class NotificationService extends BaseService { const svc_event = this.services.get('event'); [['ack', 'acknowledged'], ['read', 'read']].forEach(([ep_name, col_name]) => { - Endpoint({ - route: `/mark-${ ep_name}`, - methods: ['POST'], - handler: async (req, res) => { - // TODO: validate uid - if ( typeof req.body.uid !== 'string' ) { - throw APIError.create('field_invalid', null, { - key: 'uid', - expected: 'a valid UUID', - got: 'non-string value', - }); - } + router.use(eggspress(`/mark-${ ep_name}`, { + allowedMethods: ['POST'], + }, async (req, res) => { + // TODO: validate uid + if ( typeof req.body.uid !== 'string' ) { + throw APIError.create('field_invalid', null, { + key: 'uid', + expected: 'a valid UUID', + got: 'non-string value', + }); + } - const ack_ts = Math.floor(Date.now() / 1000); - await this.db.write(`UPDATE \`notification\` SET ${ col_name } = ? ` + - 'WHERE uid = ? AND user_id = ? ' + - 'LIMIT 1', - [ack_ts, req.body.uid, req.user.id]); + const ack_ts = Math.floor(Date.now() / 1000); + await this.db.write( + `UPDATE \`notification\` SET ${ col_name } = ? ` + + 'WHERE uid = ? AND user_id = ? ' + + 'LIMIT 1', + [ack_ts, req.body.uid, req.user.id], + ); - svc_event.emit('outer.gui.notif.ack', { - user_id_list: [req.user.id], - response: { - uid: req.body.uid, - }, - }); + svc_event.emit('outer.gui.notif.ack', { + user_id_list: [req.user.id], + response: { + uid: req.body.uid, + }, + }); - res.json({}); - }, - }).attach(router); + res.json({}); + })); }); } @@ -192,17 +192,21 @@ class NotificationService extends BaseService { */ async do_on_user_connected ({ user }) { // query the users unread notifications - const notifications = await this.db.read('SELECT * FROM `notification` ' + + const notifications = await this.db.read( + 'SELECT * FROM `notification` ' + 'WHERE user_id=? AND shown IS NULL AND acknowledged IS NULL ' + 'ORDER BY created_at ASC', - [user.id]); + [user.id], + ); // set all the notifications to "shown" const shown_ts = Math.floor(Date.now() / 1000); - await this.db.write('UPDATE `notification` ' + + await this.db.write( + 'UPDATE `notification` ' + 'SET shown = ? ' + 'WHERE user_id=? AND shown IS NULL AND acknowledged IS NULL ', - [shown_ts, user.id]); + [shown_ts, user.id], + ); for ( const n of notifications ) { n.value = this.db.case({ @@ -288,10 +292,12 @@ class NotificationService extends BaseService { (async () => { for ( const user_id of user_id_list ) { - await this.db.write('INSERT INTO `notification` ' + + await this.db.write( + 'INSERT INTO `notification` ' + '(`user_id`, `uid`, `value`) ' + 'VALUES (?, ?, ?)', - [user_id, uid, JSON.stringify(notification)]); + [user_id, uid, JSON.stringify(notification)], + ); } const p = this.notifs_pending_write[uid]; delete this.notifs_pending_write[uid]; diff --git a/src/backend/src/services/PeerService.js b/src/backend/src/services/PeerService.js index a117db8347..6bec122b12 100644 --- a/src/backend/src/services/PeerService.js +++ b/src/backend/src/services/PeerService.js @@ -18,7 +18,7 @@ */ import configurable_auth from '../middleware/configurable_auth.js'; -import { Endpoint } from '../util/expressutil.js'; +import eggspress from '../api/eggspress.js'; import { Actor, UserActorType } from './auth/Actor.js'; import BaseService from './BaseService.js'; @@ -28,93 +28,87 @@ function addDashesToUUID (i) { export class PeerService extends BaseService { '__on_install.routes' (_, { app }) { - Endpoint({ - route: '/peer/signaller-info', - methods: ['GET'], + app.use(eggspress('/peer/signaller-info', { + allowedMethods: ['GET'], subdomain: 'api', - handler: async (req, res) => { - res.json({ - url: this.config.signaller_url, - fallbackIce: this.config.fallback_ice, - }); - }, - }).attach(app); + }, async (req, res) => { + res.json({ + url: this.config.signaller_url, + fallbackIce: this.config.fallback_ice, + }); + })); - Endpoint({ - route: '/peer/generate-turn', - methods: ['POST'], + app.use(eggspress('/peer/generate-turn', { + allowedMethods: ['POST'], mw: [configurable_auth()], subdomain: 'api', - handler: async (req, res) => { - if ( ! this.config.cloudflare_turn ) { - res.status(500).send({ error: 'TURN is not configured' }); - return; - } + }, async (req, res) => { + if ( ! this.config.cloudflare_turn ) { + res.status(500).send({ error: 'TURN is not configured' }); + return; + } - // Build the custom identifier (short max length, we must compress it from hex to b64) - let customIdentifier = ''; - customIdentifier += Buffer.from(req.actor.type.user.uuid.replaceAll('-', ''), 'hex').toString('base64url'); - if ( req.actor.type?.app ) { - customIdentifier += `:${ Buffer.from(req.actor.type.app.uid.replace('app-', '').replaceAll('-', ''), 'hex').toString('base64url')}`; - } - let response = await fetch( - `https://rtc.live.cloudflare.com/v1/turn/keys/${this.config.cloudflare_turn.turn_key_id}/credentials/generate-ice-servers`, - { - headers: { - Authorization: `Bearer ${this.config.cloudflare_turn.turn_key_api_token}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - ttl: this.config.cloudflare_turn.ttl_ms, - customIdentifier, - }), + // Build the custom identifier (short max length, we must compress it from hex to b64) + let customIdentifier = ''; + customIdentifier += Buffer.from(req.actor.type.user.uuid.replaceAll('-', ''), 'hex').toString('base64url'); + if ( req.actor.type?.app ) { + customIdentifier += `:${ Buffer.from(req.actor.type.app.uid.replace('app-', '').replaceAll('-', ''), 'hex').toString('base64url')}`; + } + let response = await fetch( + `https://rtc.live.cloudflare.com/v1/turn/keys/${this.config.cloudflare_turn.turn_key_id}/credentials/generate-ice-servers`, + { + headers: { + Authorization: `Bearer ${this.config.cloudflare_turn.turn_key_api_token}`, + 'Content-Type': 'application/json', }, - ); + method: 'POST', + body: JSON.stringify({ + ttl: this.config.cloudflare_turn.ttl_ms, + customIdentifier, + }), + }, + ); - if ( ! response.ok ) { - res.status(500).send({ error: 'Failed to generate TURN credentials' }); - return; - } + if ( ! response.ok ) { + res.status(500).send({ error: 'Failed to generate TURN credentials' }); + return; + } - const { iceServers } = await response.json(); + const { iceServers } = await response.json(); - res.json({ - ttl: this.config.cloudflare_turn.ttl_ms, - iceServers, - }); - }, - }).attach(app); + res.json({ + ttl: this.config.cloudflare_turn.ttl_ms, + iceServers, + }); + })); const svc_web = this.services.get('web-server'); const meteringService = this.services.get('meteringService').meteringService; svc_web.allow_undefined_origin('/turn/ingest-usage'); - Endpoint({ - route: '/turn/ingest-usage', - methods: ['POST'], + app.use(eggspress('/turn/ingest-usage', { + allowedMethods: ['POST'], subdomain: 'api', - handler: async (req, res) => { - if ( req.headers['x-puter-internal-auth'] !== this.config.turn_meter_secret ) { - res.status(403).send({ error: 'Failed to meter TURN credentials' }); - return; - } - /** @type {{timestamp: string, userId: string, origin: string, customIdentifier: Number, egressBytes: number, ingressBytes: number}[]} */ - const records = req.body.records; - for ( const record of records ) { - try { - const actor = await Actor.create(UserActorType, { - user_uid: addDashesToUUID(Buffer.from(record.userId, 'base64url').toString('hex')), - }); - const costInMicrocents = record.egressBytes * 0.005; - meteringService.incrementUsage(actor, 'turn:egress-bytes', record.egressBytes, costInMicrocents); - } catch (e) { - // failed to get user likely - console.error('TURN metering error: ', e); - } - res.send('ok'); + }, async (req, res) => { + if ( req.headers['x-puter-internal-auth'] !== this.config.turn_meter_secret ) { + res.status(403).send({ error: 'Failed to meter TURN credentials' }); + return; + } + /** @type {{timestamp: string, userId: string, origin: string, customIdentifier: Number, egressBytes: number, ingressBytes: number}[]} */ + const records = req.body.records; + for ( const record of records ) { + try { + const actor = await Actor.create(UserActorType, { + user_uid: addDashesToUUID(Buffer.from(record.userId, 'base64url').toString('hex')), + }); + const costInMicrocents = record.egressBytes * 0.005; + meteringService.incrementUsage(actor, 'turn:egress-bytes', record.egressBytes, costInMicrocents); + } catch (e) { + // failed to get user likely + console.error('TURN metering error: ', e); } - }, - }).attach(app); + res.send('ok'); + } + })); } } diff --git a/src/backend/src/services/PermissionAPIService.js b/src/backend/src/services/PermissionAPIService.js index 94eb6d7f18..2ac1dc2f4f 100644 --- a/src/backend/src/services/PermissionAPIService.js +++ b/src/backend/src/services/PermissionAPIService.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ const { APIError } = require('openai'); +const eggspress = require('../api/eggspress'); const configurable_auth = require('../middleware/configurable_auth'); -const { Endpoint } = require('../util/expressutil'); const BaseService = require('./BaseService'); /** @@ -55,9 +55,15 @@ class PermissionAPIService extends BaseService { app.use(require('../routers/auth/check-permissions.js')); app.use(require('../routers/auth/request-app-root-dir')); - Endpoint(require('../routers/auth/check-app-acl.endpoint.js')).but({ - route: '/auth/check-app-acl', - }).attach(app); + const checkAppAclSpec = require('../routers/auth/check-app-acl.endpoint.js'); + app.use(eggspress('/auth/check-app-acl', { + allowedMethods: checkAppAclSpec.methods ?? ['GET'], + ...(checkAppAclSpec.subdomain ? { subdomain: checkAppAclSpec.subdomain } : {}), + ...(checkAppAclSpec.parameters ? { parameters: checkAppAclSpec.parameters } : {}), + ...(checkAppAclSpec.alias ? { alias: checkAppAclSpec.alias } : {}), + ...(checkAppAclSpec.mw ? { mw: checkAppAclSpec.mw } : {}), + ...checkAppAclSpec.otherOpts, + }, checkAppAclSpec.handler)); // track: scoping iife /** @@ -76,174 +82,164 @@ class PermissionAPIService extends BaseService { } install_group_endpoints_ ({ router }) { - Endpoint({ - route: '/create', - methods: ['POST'], + router.use(eggspress('/create', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - const owner_user_id = req.user.id; - - const extra = req.body.extra ?? {}; - const metadata = req.body.metadata ?? {}; - if ( !extra || typeof extra !== 'object' || Array.isArray(extra) ) { - throw APIError.create('field_invalid', null, { - key: 'extra', - expected: 'object', - got: extra, - }); - } - if ( !metadata || typeof metadata !== 'object' || Array.isArray(metadata) ) { - throw APIError.create('field_invalid', null, { - key: 'metadata', - expected: 'object', - got: metadata, - }); - } - - const svc_group = this.services.get('group'); - const uid = await svc_group.create({ - owner_user_id, - // TODO: includeslist for allowed 'extra' fields - extra: {}, - // Metadata can be specified in request - metadata: metadata ?? {}, + }, async (req, res) => { + const owner_user_id = req.user.id; + + const extra = req.body.extra ?? {}; + const metadata = req.body.metadata ?? {}; + if ( !extra || typeof extra !== 'object' || Array.isArray(extra) ) { + throw APIError.create('field_invalid', null, { + key: 'extra', + expected: 'object', + got: extra, }); + } + if ( !metadata || typeof metadata !== 'object' || Array.isArray(metadata) ) { + throw APIError.create('field_invalid', null, { + key: 'metadata', + expected: 'object', + got: metadata, + }); + } + + const svc_group = this.services.get('group'); + const uid = await svc_group.create({ + owner_user_id, + // TODO: includeslist for allowed 'extra' fields + extra: {}, + // Metadata can be specified in request + metadata: metadata ?? {}, + }); + + res.json({ uid }); + })); + + router.use(eggspress('/add-users', { + allowedMethods: ['POST'], + mw: [configurable_auth()], + }, async (req, res) => { + const svc_group = this.services.get('group'); - res.json({ uid }); - }, - }).attach(router); + // TODO: validate string and uuid for request - Endpoint({ - route: '/add-users', - methods: ['POST'], - mw: [configurable_auth()], - handler: async (req, res) => { - const svc_group = this.services.get('group'); - - // TODO: validate string and uuid for request - - const group = await svc_group.get({ uid: req.body.uid }); - - if ( ! group ) { - throw APIError.create('entity_not_found', null, { - identifier: req.body.uid, - }); - } - - if ( group.owner_user_id !== req.user.id ) { - throw APIError.create('forbidden'); - } - - if ( ! Array.isArray(req.body.users) ) { - throw APIError.create('field_invalid', null, { - key: 'users', - expected: 'array', - got: req.body.users, - }); - } - - for ( let i = 0 ; i < req.body.users.length ; i++ ) { - const value = req.body.users[i]; - if ( typeof value === 'string' ) continue; - throw APIError.create('field_invalid', null, { - key: `users[${i}]`, - expected: 'string', - got: value, - }); - } - - await svc_group.add_users({ - uid: req.body.uid, - users: req.body.users, + const group = await svc_group.get({ uid: req.body.uid }); + + if ( ! group ) { + throw APIError.create('entity_not_found', null, { + identifier: req.body.uid, + }); + } + + if ( group.owner_user_id !== req.user.id ) { + throw APIError.create('forbidden'); + } + + if ( ! Array.isArray(req.body.users) ) { + throw APIError.create('field_invalid', null, { + key: 'users', + expected: 'array', + got: req.body.users, + }); + } + + for ( let i = 0 ; i < req.body.users.length ; i++ ) { + const value = req.body.users[i]; + if ( typeof value === 'string' ) continue; + throw APIError.create('field_invalid', null, { + key: `users[${i}]`, + expected: 'string', + got: value, }); + } - res.json({}); - }, - }).attach(router); + await svc_group.add_users({ + uid: req.body.uid, + users: req.body.users, + }); + + res.json({}); + })); // TODO: DRY: add-users is very similar - Endpoint({ - route: '/remove-users', - methods: ['POST'], + router.use(eggspress('/remove-users', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - const svc_group = this.services.get('group'); - - // TODO: validate string and uuid for request - - const group = await svc_group.get({ uid: req.body.uid }); - - if ( ! group ) { - throw APIError.create('entity_not_found', null, { - identifier: req.body.uid, - }); - } - - if ( group.owner_user_id !== req.user.id ) { - throw APIError.create('forbidden'); - } - - if ( Array.isArray(req.body.users) ) { - throw APIError.create('field_invalid', null, { - key: 'users', - expected: 'array', - got: req.body.users, - }); - } - - for ( let i = 0 ; i < req.body.users.length ; i++ ) { - const value = req.body.users[i]; - if ( typeof value === 'string' ) continue; - throw APIError.create('field_invalid', null, { - key: `users[${i}]`, - expected: 'string', - got: value, - }); - } - - await svc_group.remove_users({ - uid: req.body.uid, - users: req.body.users, + }, async (req, res) => { + const svc_group = this.services.get('group'); + + // TODO: validate string and uuid for request + + const group = await svc_group.get({ uid: req.body.uid }); + + if ( ! group ) { + throw APIError.create('entity_not_found', null, { + identifier: req.body.uid, }); + } + + if ( group.owner_user_id !== req.user.id ) { + throw APIError.create('forbidden'); + } - res.json({}); - }, - }).attach(router); + if ( Array.isArray(req.body.users) ) { + throw APIError.create('field_invalid', null, { + key: 'users', + expected: 'array', + got: req.body.users, + }); + } + + for ( let i = 0 ; i < req.body.users.length ; i++ ) { + const value = req.body.users[i]; + if ( typeof value === 'string' ) continue; + throw APIError.create('field_invalid', null, { + key: `users[${i}]`, + expected: 'string', + got: value, + }); + } - Endpoint({ - route: '/list', - methods: ['GET'], + await svc_group.remove_users({ + uid: req.body.uid, + users: req.body.users, + }); + + res.json({}); + })); + + router.use(eggspress('/list', { + allowedMethods: ['GET'], mw: [configurable_auth()], - handler: async (req, res) => { - const svc_group = this.services.get('group'); + }, async (req, res) => { + const svc_group = this.services.get('group'); - // TODO: validate string and uuid for request + // TODO: validate string and uuid for request - const owned_groups = await svc_group.list_groups_with_owner({ owner_user_id: req.user.id }); + const owned_groups = await svc_group.list_groups_with_owner({ owner_user_id: req.user.id }); - const in_groups = await svc_group.list_groups_with_member({ user_id: req.user.id }); + const in_groups = await svc_group.list_groups_with_member({ user_id: req.user.id }); - const public_groups = await svc_group.list_public_groups(); + const public_groups = await svc_group.list_public_groups(); - res.json({ - owned_groups: await Promise.all(owned_groups.map(g => g.get_client_value({ members: true }))), - in_groups: await Promise.all(in_groups.map(g => g.get_client_value({ members: true }))), - public_groups: await Promise.all(public_groups.map(g => g.get_client_value())), - }); - }, - }).attach(router); + res.json({ + owned_groups: await Promise.all(owned_groups.map(g => g.get_client_value({ members: true }))), + in_groups: await Promise.all(in_groups.map(g => g.get_client_value({ members: true }))), + public_groups: await Promise.all(public_groups.map(g => g.get_client_value())), + }); + })); - Endpoint({ - route: '/public-groups', - methods: ['GET'], + router.use(eggspress('/public-groups', { + allowedMethods: ['GET'], mw: [configurable_auth()], - handler: async (req, res) => { - res.json({ - user: this.global_config.default_user_group, - temp: this.global_config.default_temp_group, - }); - }, - }).attach(router); + }, async (req, res) => { + res.json({ + user: this.global_config.default_user_group, + temp: this.global_config.default_temp_group, + }); + })); } } diff --git a/src/backend/src/services/PuterAPIService.js b/src/backend/src/services/PuterAPIService.js index 76dcec60c5..b661793424 100644 --- a/src/backend/src/services/PuterAPIService.js +++ b/src/backend/src/services/PuterAPIService.js @@ -17,6 +17,7 @@ * along with this program. If not, see . */ import configurable_auth from '../middleware/configurable_auth.js'; +import eggspress from '../api/eggspress.js'; import appsRouter from '../routers/apps.js'; import authAppUidFromOriginRouter from '../routers/auth/app-uid-from-origin.js'; import authCheckAppRouter from '../routers/auth/check-app.js'; @@ -62,7 +63,6 @@ import suggestAppsRouter from '../routers/suggest_apps.js'; import testRouter from '../routers/test.js'; import updateTaskbarItemsRouter from '../routers/update-taskbar-items.js'; import verifyPassRecoveryTokenRouter from '../routers/verify-pass-recovery-token.js'; -import { Endpoint } from '../util/expressutil.js'; import BaseService from './BaseService.js'; /** * @class PuterAPIService @@ -132,12 +132,10 @@ export class PuterAPIService extends BaseService { app.use(testRouter); app.use(updateTaskbarItemsRouter); - Endpoint({ - route: '/get-launch-apps', - methods: ['GET'], + app.use(eggspress('/get-launch-apps', { + allowedMethods: ['GET'], mw: [configurable_auth()], - handler: launchAppsHandler, - }).attach(app); + }, launchAppsHandler)); } } diff --git a/src/backend/src/services/PuterHomepageService.js b/src/backend/src/services/PuterHomepageService.js index e882ffce04..1a50e394fb 100644 --- a/src/backend/src/services/PuterHomepageService.js +++ b/src/backend/src/services/PuterHomepageService.js @@ -19,8 +19,8 @@ import { encode } from 'html-entities'; import { LRUCache } from 'lru-cache'; import fs from 'node:fs'; +import eggspress from '../api/eggspress.js'; import { is_valid_url } from '../helpers.js'; -import { Endpoint } from '../util/expressutil.js'; import { PathBuilder } from '../util/pathutil.js'; import BaseService from './BaseService.js'; /** @@ -69,29 +69,27 @@ export class PuterHomepageService extends BaseService { } async '__on_install.routes' (_, { app }) { - Endpoint({ - route: '/whoarewe', - methods: ['GET'], - handler: async (req, res) => { - // Get basic configuration information - const responseData = { - disable_user_signup: this.global_config.disable_user_signup, - disable_temp_users: this.global_config.disable_temp_users, - environmentInfo: { - env: this.global_config.env, - version: process.env.VERSION || 'development', - }, - }; + app.use(eggspress('/whoarewe', { + allowedMethods: ['GET'], + }, async (req, res) => { + // Get basic configuration information + const responseData = { + disable_user_signup: this.global_config.disable_user_signup, + disable_temp_users: this.global_config.disable_temp_users, + environmentInfo: { + env: this.global_config.env, + version: process.env.VERSION || 'development', + }, + }; - // Add captcha requirement information - responseData.captchaRequired = { - login: req.captchaRequired, - signup: req.captchaRequired, - }; + // Add captcha requirement information + responseData.captchaRequired = { + login: req.captchaRequired, + signup: req.captchaRequired, + }; - res.json(responseData); - }, - }).attach(app); + res.json(responseData); + })); } /** diff --git a/src/backend/src/services/SNSService.js b/src/backend/src/services/SNSService.js index 6e774f2aa6..fb62554743 100644 --- a/src/backend/src/services/SNSService.js +++ b/src/backend/src/services/SNSService.js @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -const { Endpoint } = require('../util/expressutil'); +const eggspress = require('../api/eggspress'); const BaseService = require('./BaseService'); const { LRUCache: LRU } = require('lru-cache'); @@ -64,86 +64,84 @@ class SNSService extends BaseService { } async '__on_install.routes' (_, { app }) { - Endpoint({ - route: '/sns', - methods: ['POST'], - handler: async (req, res) => { - const message = req.body; - - const REQUIRED_FIELDS = ['SignatureVersion', 'SigningCertURL', 'Type', 'Signature']; - for ( const field of REQUIRED_FIELDS ) { - if ( ! message[field] ) { - this.log.info('SES response', { status: 400, because: 'missing field', field }); - res.status(400).send(`Missing required field: ${field}`); - return; - } - } - - if ( ! SNS_TYPES[message.Type] ) { - this.log.info('SES response', { - status: 400, - because: 'invalid Type', - value: message.Type, - }); - res.status(400).send('Invalid SNS message type'); + app.use(eggspress('/sns', { + allowedMethods: ['POST'], + }, async (req, res) => { + const message = req.body; + + const REQUIRED_FIELDS = ['SignatureVersion', 'SigningCertURL', 'Type', 'Signature']; + for ( const field of REQUIRED_FIELDS ) { + if ( ! message[field] ) { + this.log.info('SES response', { status: 400, because: 'missing field', field }); + res.status(400).send(`Missing required field: ${field}`); return; } + } - if ( message.SignatureVersion !== '1' ) { - this.log.info('SES response', { - status: 400, - because: 'invalid SignatureVersion', - value: message.SignatureVersion, - }); - res.status(400).send('Invalid SignatureVersion'); - return; - } + if ( ! SNS_TYPES[message.Type] ) { + this.log.info('SES response', { + status: 400, + because: 'invalid Type', + value: message.Type, + }); + res.status(400).send('Invalid SNS message type'); + return; + } - if ( ! CERT_URL_PATTERN.test(message.SigningCertURL) ) { - this.log.info('SES response', { - status: 400, - because: 'invalid SigningCertURL', - value: message.SignatureVersion, - }); - throw Error('Invalid certificate URL'); - } + if ( message.SignatureVersion !== '1' ) { + this.log.info('SES response', { + status: 400, + because: 'invalid SignatureVersion', + value: message.SignatureVersion, + }); + res.status(400).send('Invalid SignatureVersion'); + return; + } - const topic_arns = this.config?.topic_arns ?? []; - if ( ! topic_arns.includes(message.TopicArn) ) { - this.log.info('SES response', { - status: 403, - because: 'invalid TopicArn', - value: message.TopicArn, - }); - res.status(403).send('Invalid TopicArn'); - return; - } + if ( ! CERT_URL_PATTERN.test(message.SigningCertURL) ) { + this.log.info('SES response', { + status: 400, + because: 'invalid SigningCertURL', + value: message.SignatureVersion, + }); + throw Error('Invalid certificate URL'); + } - if ( ! await this.verify_message_(message) ) { - this.log.info('SES response', { - status: 403, - because: 'message signature validation', - value: message.SignatureVersion, - }); - res.status(403).send('Invalid signature'); - return; - } + const topic_arns = this.config?.topic_arns ?? []; + if ( ! topic_arns.includes(message.TopicArn) ) { + this.log.info('SES response', { + status: 403, + because: 'invalid TopicArn', + value: message.TopicArn, + }); + res.status(403).send('Invalid TopicArn'); + return; + } - if ( message.Type === 'SubscriptionConfirmation' ) { - // Confirm subscription - const response = await axios.get(message.SubscribeURL); - if ( response.status !== 200 ) { - res.status(500).send('Failed to confirm subscription'); - return; - } + if ( ! await this.verify_message_(message) ) { + this.log.info('SES response', { + status: 403, + because: 'message signature validation', + value: message.SignatureVersion, + }); + res.status(403).send('Invalid signature'); + return; + } + + if ( message.Type === 'SubscriptionConfirmation' ) { + // Confirm subscription + const response = await axios.get(message.SubscribeURL); + if ( response.status !== 200 ) { + res.status(500).send('Failed to confirm subscription'); + return; } + } - const svc_event = this.services.get('event'); - this.log.info('SNS message', { message }); - svc_event.emit('sns', { message }); - res.status(200).send('Thanks SNS'); - }, - }).attach(app); + const svc_event = this.services.get('event'); + this.log.info('SNS message', { message }); + svc_event.emit('sns', { message }); + res.status(200).send('Thanks SNS'); + })); } async verify_message_ (message, options = {}) { diff --git a/src/backend/src/services/ShareService.js b/src/backend/src/services/ShareService.js index a01722a99f..490e653efe 100644 --- a/src/backend/src/services/ShareService.js +++ b/src/backend/src/services/ShareService.js @@ -17,9 +17,9 @@ * along with this program. If not, see . */ const APIError = require('../api/APIError'); +const eggspress = require('../api/eggspress'); const { get_user } = require('../helpers'); const configurable_auth = require('../middleware/configurable_auth'); -const { Endpoint } = require('../util/expressutil'); const { Actor, UserActorType } = require('./auth/Actor'); const BaseService = require('./BaseService'); const { DB_WRITE } = require('./database/consts'); @@ -52,8 +52,10 @@ class ShareService extends BaseService { const svc_event = this.services.get('event'); svc_event.on('user.email-confirmed', async (_, { user_uid, email }) => { const user = await get_user({ uuid: user_uid }); - const relevant_shares = await this.db.read('SELECT * FROM share WHERE recipient_email = ?', - [email]); + const relevant_shares = await this.db.read( + 'SELECT * FROM share WHERE recipient_email = ?', + [email], + ); for ( const share of relevant_shares ) { share.data = this.db.case({ @@ -80,8 +82,10 @@ class ShareService extends BaseService { await svc_acl.set_user_user(issuer_actor, user.username, permission, undefined, { only_if_higher: true }); } - await this.db.write('DELETE FROM share WHERE uid = ?', - [share.uid]); + await this.db.write( + 'DELETE FROM share WHERE uid = ?', + [share.uid], + ); } }); } @@ -113,179 +117,179 @@ class ShareService extends BaseService { 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', - }); - } + router.use(eggspress('/check', { + allowedMethods: ['POST'], + }, 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 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, - }); + const share = await svc_share.get_share({ + uid: decoded.uid, + }); - if ( ! share ) { - throw APIError.create('invalid_token'); - } + if ( ! share ) { + throw APIError.create('invalid_token'); + } - res.json({ - $: 'api:share', - uid: share.uid, - email: share.recipient_email, - }); - }, - }).attach(router); + res.json({ + $: 'api:share', + uid: share.uid, + email: share.recipient_email, + }); + })); - Endpoint({ - route: '/apply', - methods: ['POST'], + router.use(eggspress('/apply', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - const share_uid = req.body.uid; + }, async (req, res) => { + const share_uid = req.body.uid; - const share = await svc_share.get_share({ - uid: share_uid, - }); + const share = await svc_share.get_share({ + uid: share_uid, + }); - if ( ! share ) { - throw APIError.create('share_expired'); - } + if ( ! share ) { + throw APIError.create('share_expired'); + } - share.data = this.db.case({ - mysql: () => share.data, - otherwise: () => - JSON.parse(share.data ?? '{}'), - })(); + share.data = this.db.case({ + mysql: () => share.data, + otherwise: () => + JSON.parse(share.data ?? '{}'), + })(); - const actor = Actor.adapt(req.actor ?? req.user); - if ( ! actor ) { - // this shouldn't happen; auth should catch it - throw new Error('actor missing'); - } + 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_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'); - } + 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, - }); + const issuer_user = await get_user({ + id: share.issuer_user_id, + }); - if ( ! issuer_user ) { - throw APIError.create('share_expired'); - } + if ( ! issuer_user ) { + throw APIError.create('share_expired'); + } - const issuer_actor = await Actor.create(UserActorType, { - user: issuer_user, - }); + const issuer_actor = await Actor.create(UserActorType, { + user: issuer_user, + }); - const svc_permission = this.services.get('permission'); + 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); - } + for ( const permission of share.data.permissions ) { + await svc_permission.grant_user_user_permission( + issuer_actor, + actor.type.user.username, + permission, + ); + } - await this.db.write('DELETE FROM share WHERE uid = ?', - [share.uid]); + await this.db.write( + 'DELETE FROM share WHERE uid = ?', + [share.uid], + ); - res.json({ - $: 'api:status-report', - status: 'success', - }); - }, - }).attach(router); + res.json({ + $: 'api:status-report', + status: 'success', + }); + })); - Endpoint({ - route: '/request', - methods: ['POST'], + router.use(eggspress('/request', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - const share_uid = req.body.uid; + }, async (req, res) => { + const share_uid = req.body.uid; - const share = await svc_share.get_share({ - uid: share_uid, - }); + const share = await svc_share.get_share({ + uid: share_uid, + }); - // track: null check before processing - if ( ! share ) { - throw APIError.create('share_expired'); - } + // track: null check before processing + if ( ! share ) { + throw APIError.create('share_expired'); + } - share.data = this.db.case({ - mysql: () => share.data, - otherwise: () => - JSON.parse(share.data ?? '{}'), - })(); + share.data = this.db.case({ + mysql: () => share.data, + otherwise: () => + JSON.parse(share.data ?? '{}'), + })(); - const actor = Actor.adapt(req.actor ?? req.user); - if ( ! actor ) { - // this shouldn't happen; auth should catch it - throw new Error('actor missing'); - } + const actor = Actor.adapt(req.actor ?? req.user); + if ( ! actor ) { + // this shouldn't happen; auth should catch it + throw new Error('actor missing'); + } - // track: opposite condition of sibling - // :: sibling: /apply endpoint - if ( - actor.type.user.email_confirmed && - actor.type.user.email === share.recipient_email - ) { - throw APIError.create('no_need_to_request'); - } + // track: opposite condition of sibling + // :: sibling: /apply endpoint + if ( + actor.type.user.email_confirmed && + actor.type.user.email === share.recipient_email + ) { + throw APIError.create('no_need_to_request'); + } - const issuer_user = await get_user({ - id: share.issuer_user_id, - }); + const issuer_user = await get_user({ + id: share.issuer_user_id, + }); - if ( ! issuer_user ) { - throw APIError.create('share_expired'); - } + if ( ! issuer_user ) { + throw APIError.create('share_expired'); + } - const svc_notification = this.services.get('notification'); - svc_notification.notify(UsernameNotifSelector(issuer_user.username), - { - source: 'sharing', - title: `User ${actor.type.user.username} is ` + - `trying to open a share you sent to ${ - share.recipient_email}`, - template: 'user-requesting-share', - fields: { - username: actor.type.user.username, - intended_recipient: share.recipient_email, - permissions: share.data.permissions, - }, - }); - res.json({ - $: 'api:status-report', - status: 'success', - }); - }, - }).attach(router); + const svc_notification = this.services.get('notification'); + svc_notification.notify( + UsernameNotifSelector(issuer_user.username), + { + source: 'sharing', + title: `User ${actor.type.user.username} is ` + + `trying to open a share you sent to ${ + share.recipient_email}`, + template: 'user-requesting-share', + fields: { + username: actor.type.user.username, + intended_recipient: share.recipient_email, + permissions: share.data.permissions, + }, + }, + ); + res.json({ + $: 'api:status-report', + status: 'success', + }); + })); } install_share_endpoint ({ app }) { @@ -299,40 +303,40 @@ class ShareService extends BaseService { app.use('/share', router); const share_sequence = require('../structured/sequence/share.js'); - Endpoint({ - route: '/', - methods: ['POST'], + router.use(eggspress('/', { + allowedMethods: ['POST'], mw: [ configurable_auth(), // featureflag({ feature: 'share' }), ], - handler: async (req, res) => { - const svc_edgeRateLimit = req.services.get('edge-rate-limit'); - if ( ! svc_edgeRateLimit.check('verify-pass-recovery-token') ) { - return res.status(429).send('Too many requests.'); - } - - const actor = req.actor; - if ( ! (actor.type instanceof UserActorType) ) { - throw APIError.create('forbidden'); - } + }, async (req, res) => { + const svc_edgeRateLimit = req.services.get('edge-rate-limit'); + if ( ! svc_edgeRateLimit.check('verify-pass-recovery-token') ) { + return res.status(429).send('Too many requests.'); + } - if ( ! actor.type.user.email_confirmed ) { - throw APIError.create('email_must_be_confirmed', null, { - action: 'share something', - }); - } + const actor = req.actor; + if ( ! (actor.type instanceof UserActorType) ) { + throw APIError.create('forbidden'); + } - return await share_sequence.call(this, { - actor, req, res, + if ( ! actor.type.user.email_confirmed ) { + throw APIError.create('email_must_be_confirmed', null, { + action: 'share something', }); - }, - }).attach(router); + } + + return await share_sequence.call(this, { + actor, req, res, + }); + })); } async get_share ({ uid }) { - const [share] = await this.db.read('SELECT * FROM share WHERE uid = ?', - [uid]); + const [share] = await this.db.read( + 'SELECT * FROM share WHERE uid = ?', + [uid], + ); return share; } @@ -384,10 +388,12 @@ class ShareService extends BaseService { const uuid = this.modules.uuidv4(); - await this.db.write('INSERT INTO `share` ' + + await this.db.write( + 'INSERT INTO `share` ' + '(`uid`, `issuer_user_id`, `recipient_email`, `data`) ' + 'VALUES (?, ?, ?, ?)', - [uuid, issuer.type.user.id, email, JSON.stringify(data)]); + [uuid, issuer.type.user.id, email, JSON.stringify(data)], + ); return uuid; } diff --git a/src/backend/src/services/WebDAV/WebDAVService.js b/src/backend/src/services/WebDAV/WebDAVService.js index c081d7cdb1..945831e9e6 100644 --- a/src/backend/src/services/WebDAV/WebDAVService.js +++ b/src/backend/src/services/WebDAV/WebDAVService.js @@ -17,8 +17,8 @@ * along with this program. If not, see . */ const { NodePathSelector } = require('../../filesystem/node/selectors'); +const eggspress = require('../../api/eggspress'); const configurable_auth = require('../../middleware/configurable_auth'); -const { Endpoint } = require('../../util/expressutil'); const BaseService = require('../BaseService'); const bcrypt = require('bcrypt'); const xmlparser = require('express-xml-bodyparser'); @@ -127,9 +127,11 @@ class WebDAVService extends BaseService { const { token } = await svc_auth.create_session_token(user); if ( user.otp_enabled ) { const svc_otp = this.services.get('otp'); - const ok = svc_otp.verify(user.username, - user.otp_secret, - otpToken); + const ok = svc_otp.verify( + user.username, + user.otp_secret, + otpToken, + ); if ( ! ok ) { return null; } @@ -155,16 +157,20 @@ class WebDAVService extends BaseService { try { // Parse Basic auth credentials const base64Credentials = authHeader.split(' ')[1]; - const credentials = Buffer.from(base64Credentials, - 'base64').toString( 'ascii'); + const credentials = Buffer.from( + base64Credentials, + 'base64', + ).toString( 'ascii'); let [username, ...password] = credentials.split(':'); password = password.join(':'); // Call user's authentication function - actor = await this.authenticateWebDavUser(username, - password, - req, - res); + actor = await this.authenticateWebDavUser( + username, + password, + req, + res, + ); if ( ! actor ) { // Authentication failed res.set({ @@ -220,31 +226,32 @@ class WebDAVService extends BaseService { app.use('/', r_webdav); - Endpoint({ - subdomain: 'dav', - route: '/*', - methods: [ - 'PROPFIND', - 'PROPPATCH', - 'MKCOL', - 'GET', - 'HEAD', - 'POST', - 'PUT', - 'DELETE', - 'COPY', - 'MOVE', - 'LOCK', - 'UNLOCK', - 'OPTIONS', - ], - mw: [configurable_auth({ optional: true })], + r_webdav.use(eggspress( + '/*', + { + subdomain: 'dav', + allowedMethods: [ + 'PROPFIND', + 'PROPPATCH', + 'MKCOL', + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'COPY', + 'MOVE', + 'LOCK', + 'UNLOCK', + 'OPTIONS', + ], + mw: [configurable_auth({ optional: true })], + }, /** - * - * @param {import("express").Request} req - * @param {import("express").Response} res - */ - handler: async ( req, res ) => { + * + * @param {import("express").Request} req + * @param {import("express").Response} res + */ async ( req, res ) => { if ( req.method === 'OPTIONS' ) { this.handleWebDavServer('/', req, res); return; @@ -264,7 +271,7 @@ class WebDAVService extends BaseService { this.handleWebDavServer(filePath, req, res); }); }, - }).attach( r_webdav); + )); } } diff --git a/src/backend/src/services/WispService.js b/src/backend/src/services/WispService.js index 8939ef689e..7aad2bedd4 100644 --- a/src/backend/src/services/WispService.js +++ b/src/backend/src/services/WispService.js @@ -18,7 +18,7 @@ */ const configurable_auth = require('../middleware/configurable_auth'); -const { Endpoint } = require('../util/expressutil'); +const eggspress = require('../api/eggspress'); const BaseService = require('./BaseService'); class WispService extends BaseService { @@ -31,84 +31,80 @@ class WispService extends BaseService { app.use('/wisp', r_wisp); - Endpoint({ - route: '/relay-token/create', - methods: ['POST'], + r_wisp.use(eggspress('/relay-token/create', { + allowedMethods: ['POST'], mw: [configurable_auth({ optional: true })], - handler: async (req, res) => { - const svc_token = this.services.get('token'); - const actor = req.actor; + }, async (req, res) => { + const svc_token = this.services.get('token'); + const actor = req.actor; - if ( actor ) { - const token = svc_token.sign('wisp', { - $: 'token:wisp', - $v: '0.0.0', - user_uid: actor.type.user.uuid, - }, { - expiresIn: '1d', - }); - this.log.info('creating wisp token', { - actor: actor.uid, - token: token, - }); - res.json({ - token, - server: this.config.server, - }); - } else { - const token = svc_token.sign('wisp', { - $: 'token:wisp', - $v: '0.0.0', - guest: true, - }, { - expiresIn: '1d', - }); - res.json({ - token, - server: this.config.server, - }); - } - }, - }).attach(r_wisp); + if ( actor ) { + const token = svc_token.sign('wisp', { + $: 'token:wisp', + $v: '0.0.0', + user_uid: actor.type.user.uuid, + }, { + expiresIn: '1d', + }); + this.log.info('creating wisp token', { + actor: actor.uid, + token: token, + }); + res.json({ + token, + server: this.config.server, + }); + } else { + const token = svc_token.sign('wisp', { + $: 'token:wisp', + $v: '0.0.0', + guest: true, + }, { + expiresIn: '1d', + }); + res.json({ + token, + server: this.config.server, + }); + } + })); - Endpoint({ - route: '/relay-token/verify', - methods: ['POST'], - handler: async (req, res) => { - const svc_token = this.services.get('token'); - const svc_apiError = this.services.get('api-error'); - const svc_event = this.services.get('event'); + r_wisp.use(eggspress('/relay-token/verify', { + allowedMethods: ['POST'], + }, async (req, res) => { + const svc_token = this.services.get('token'); + const svc_apiError = this.services.get('api-error'); + const svc_event = this.services.get('event'); - const decoded = (() => { - try { - const decoded = svc_token.verify('wisp', req.body.token); - if ( decoded.$ !== 'token:wisp' ) { - throw svc_apiError.create('invalid_token'); - } - return decoded; - } catch (e) { - throw svc_apiError.create('forbidden'); + const decoded = (() => { + try { + const decoded = svc_token.verify('wisp', req.body.token); + if ( decoded.$ !== 'token:wisp' ) { + throw svc_apiError.create('invalid_token'); } - })(); - - const svc_getUser = this.services.get('get-user'); - - const event = { - allow: true, - policy: { allow: true }, - guest: decoded.guest, - user: decoded.guest ? undefined : await svc_getUser.get_user({ - uuid: decoded.user_uid, - }), - }; - await svc_event.emit('wisp.get-policy', event); - if ( ! event.allow ) { + return decoded; + } catch (e) { throw svc_apiError.create('forbidden'); } + })(); + + const svc_getUser = this.services.get('get-user'); + + const event = { + allow: true, + policy: { allow: true }, + guest: decoded.guest, + user: decoded.guest ? undefined : await svc_getUser.get_user({ + uuid: decoded.user_uid, + }), + }; + await svc_event.emit('wisp.get-policy', event); + if ( ! event.allow ) { + throw svc_apiError.create('forbidden'); + } - res.json(event.policy); - }, - }).attach(r_wisp); + res.json(event.policy); + })); } } diff --git a/src/backend/src/services/auth/ACLService.js b/src/backend/src/services/auth/ACLService.js index 583c3b9329..e3a1a66473 100644 --- a/src/backend/src/services/auth/ACLService.js +++ b/src/backend/src/services/auth/ACLService.js @@ -18,11 +18,11 @@ */ const APIError = require('../../api/APIError'); const FSNodeParam = require('../../api/filesystem/FSNodeParam'); +const eggspress = require('../../api/eggspress'); const { NodePathSelector } = require('../../filesystem/node/selectors'); const { get_user } = require('../../helpers'); const configurable_auth = require('../../middleware/configurable_auth'); const { Context } = require('../../util/context'); -const { Endpoint } = require('../../util/expressutil'); const BaseService = require('../BaseService'); const { AppUnderUserActorType, UserActorType, Actor, SystemActorType, AccessTokenActorType } = require('./Actor'); const { DB_READ } = require('../database/consts'); @@ -121,85 +121,81 @@ class ACLService extends BaseService { app.use('/acl', r_acl); - Endpoint({ - route: '/stat-user-user', - methods: ['POST'], + r_acl.use(eggspress('/stat-user-user', { + allowedMethods: ['POST'], mw: [configurable_auth()], - handler: async (req, res) => { - // Only user actor is allowed - if ( ! (req.actor.type instanceof UserActorType) ) { - return res.status(403).json({ - error: 'forbidden', - }); - } - - const holder_user = await get_user({ - username: req.body.user, + }, async (req, res) => { + // Only user actor is allowed + if ( ! (req.actor.type instanceof UserActorType) ) { + return res.status(403).json({ + error: 'forbidden', }); + } - if ( ! holder_user ) { - throw APIError.create('user_does_not_exist', null, { - username: req.body.user, - }); - } + const holder_user = await get_user({ + username: req.body.user, + }); - const issuer = req.actor; - const holder = new Actor({ - type: new UserActorType({ - user: holder_user, - }), + if ( ! holder_user ) { + throw APIError.create('user_does_not_exist', null, { + username: req.body.user, }); + } - const node = await (new FSNodeParam('path')).consolidate({ - req, - getParam: () => req.body.resource, - }); + const issuer = req.actor; + const holder = new Actor({ + type: new UserActorType({ + user: holder_user, + }), + }); - const permissions = await this.stat_user_user(issuer, holder, node); + const node = await (new FSNodeParam('path')).consolidate({ + req, + getParam: () => req.body.resource, + }); - res.json({ permissions }); - }, - }).attach(r_acl); + const permissions = await this.stat_user_user(issuer, holder, node); - Endpoint({ - route: '/set-user-user', - methods: ['POST'], - mw: [configurable_auth()], - handler: async (req, res) => { - // Only user actor is allowed - if ( ! (req.actor.type instanceof UserActorType) ) { - return res.status(403).json({ - error: 'forbidden', - }); - } + res.json({ permissions }); + })); - const holder_user = await get_user({ - username: req.body.user, + r_acl.use(eggspress('/set-user-user', { + allowedMethods: ['POST'], + mw: [configurable_auth()], + }, async (req, res) => { + // Only user actor is allowed + if ( ! (req.actor.type instanceof UserActorType) ) { + return res.status(403).json({ + error: 'forbidden', }); + } - if ( ! holder_user ) { - throw APIError.create('user_does_not_exist', null, { - username: req.body.user, - }); - } + const holder_user = await get_user({ + username: req.body.user, + }); - const issuer = req.actor; - const holder = new Actor({ - type: new UserActorType({ - user: holder_user, - }), + if ( ! holder_user ) { + throw APIError.create('user_does_not_exist', null, { + username: req.body.user, }); + } - const node = await (new FSNodeParam('path')).consolidate({ - req, - getParam: () => req.body.resource, - }); + const issuer = req.actor; + const holder = new Actor({ + type: new UserActorType({ + user: holder_user, + }), + }); + + const node = await (new FSNodeParam('path')).consolidate({ + req, + getParam: () => req.body.resource, + }); - await this.set_user_user(issuer, holder, node, req.body.mode, req.body.options ?? {}); + await this.set_user_user(issuer, holder, node, req.body.mode, req.body.options ?? {}); - res.json({}); - }, - }).attach(r_acl); + res.json({}); + })); } /** diff --git a/src/backend/src/services/auth/AuthService.js b/src/backend/src/services/auth/AuthService.js index 9ff7e8cb7b..56181cab08 100644 --- a/src/backend/src/services/auth/AuthService.js +++ b/src/backend/src/services/auth/AuthService.js @@ -1644,60 +1644,56 @@ class AuthService extends BaseService { */ '__on_install.routes' () { const { app } = this.services.get('web-server'); + const eggspress = require('../../api/eggspress'); const config = require('../../config'); const configurable_auth = require('../../middleware/configurable_auth'); - const { Endpoint } = require('../../util/expressutil'); const svc_auth = this; - Endpoint({ - route: '/get-gui-token', - methods: ['GET'], + app.use(eggspress('/get-gui-token', { + allowedMethods: ['GET'], mw: [configurable_auth()], - handler: async (req, res) => { - if ( ! req.user ) { - return res.status(401).json({}); - } + }, async (req, res) => { + if ( ! req.user ) { + return res.status(401).json({}); + } - const actor = Context.get('actor'); - if ( ! (actor.type instanceof UserActorType) ) { - return res.status(403).json({}); - } - if ( ! actor.type.session ) { - return res.status(400).json({ error: 'No session bound to this actor' }); - } + const actor = Context.get('actor'); + if ( ! (actor.type instanceof UserActorType) ) { + return res.status(403).json({}); + } + if ( ! actor.type.session ) { + return res.status(400).json({ error: 'No session bound to this actor' }); + } - const gui_token = svc_auth.create_gui_token(actor.type.user, { uuid: actor.type.session }); - return res.json({ token: gui_token }); - }, - }).attach(app); + const gui_token = svc_auth.create_gui_token(actor.type.user, { uuid: actor.type.session }); + return res.json({ token: gui_token }); + })); // Sync HTTP-only session cookie to the user implied by the request's auth token. // Used when switching users in the UI: client sends Authorization with the new user's // GUI token; we set the session cookie so cookie-based (e.g. user-protected) requests match. - Endpoint({ - route: '/session/sync-cookie', - methods: ['GET'], + app.use(eggspress('/session/sync-cookie', { + allowedMethods: ['GET'], mw: [configurable_auth()], - handler: async (req, res) => { - if ( ! req.user ) { - return res.status(401).end(); - } - const actor = Context.get('actor'); - if ( !(actor.type instanceof UserActorType) || !actor.type.session ) { - return res.status(400).end(); - } - const session_token = svc_auth.create_session_token_for_session( - actor.type.user, - actor.type.session, - ); - res.cookie(config.cookie_name, session_token, { - sameSite: 'none', - secure: true, - httpOnly: true, - }); - return res.status(204).end(); - }, - }).attach(app); + }, async (req, res) => { + if ( ! req.user ) { + return res.status(401).end(); + } + const actor = Context.get('actor'); + if ( !(actor.type instanceof UserActorType) || !actor.type.session ) { + return res.status(400).end(); + } + const session_token = svc_auth.create_session_token_for_session( + actor.type.user, + actor.type.session, + ); + res.cookie(config.cookie_name, session_token, { + sameSite: 'none', + secure: true, + httpOnly: true, + }); + return res.status(204).end(); + })); } } diff --git a/src/backend/src/services/web/UserProtectedEndpointsService.js b/src/backend/src/services/web/UserProtectedEndpointsService.js index 9c10b275c3..037bf95c08 100644 --- a/src/backend/src/services/web/UserProtectedEndpointsService.js +++ b/src/backend/src/services/web/UserProtectedEndpointsService.js @@ -17,11 +17,9 @@ * along with this program. If not, see . */ const { get_user } = require('../../helpers'); -const auth2 = require('../../middleware/auth2'); const { Context } = require('../../util/context'); const BaseService = require('../BaseService'); const { UserActorType } = require('../auth/Actor'); -const { Endpoint } = require('../../util/expressutil'); const APIError = require('../../api/APIError.js'); const configurable_auth = require('../../middleware/configurable_auth.js'); const config = require('../../config'); @@ -167,15 +165,11 @@ class UserProtectedEndpointsService extends BaseService { return (APIError.create('password_required')).write(res); }); - Endpoint(require('../../routers/user-protected/change-password.js')).attach(router); - - Endpoint(require('../../routers/user-protected/change-email.js')).attach(router); - - Endpoint(require('../../routers/user-protected/change-username.js')).attach(router); - - Endpoint(require('../../routers/user-protected/disable-2fa.js')).attach(router); - - Endpoint(require('../../routers/user-protected/delete-own-user.js')).attach(router); + router.use(require('../../routers/user-protected/change-password.js')); + router.use(require('../../routers/user-protected/change-email.js')); + router.use(require('../../routers/user-protected/change-username.js')); + router.use(require('../../routers/user-protected/disable-2fa.js')); + router.use(require('../../routers/user-protected/delete-own-user.js')); } } diff --git a/src/backend/src/services/worker/WorkerService.js b/src/backend/src/services/worker/WorkerService.js index abbb2b6ec9..9a320ffa27 100644 --- a/src/backend/src/services/worker/WorkerService.js +++ b/src/backend/src/services/worker/WorkerService.js @@ -18,7 +18,6 @@ */ const configurable_auth = require('../../middleware/configurable_auth'); -const { Endpoint } = require('../../util/expressutil'); const BaseService = require('../BaseService'); const fs = require('node:fs'); @@ -147,37 +146,43 @@ class WorkerService extends BaseService { // Send user the appropriate notification if ( cfData.success ) { - svc_notification.notify(UsernameNotifSelector(actor.type.user.username), - { - source: 'worker', - title: `Succesfully deployed ${cfData.url}`, - template: 'user-requesting-share', - fields: { - username: actor.type.user.username, - }, - }); + svc_notification.notify( + UsernameNotifSelector(actor.type.user.username), + { + source: 'worker', + title: `Succesfully deployed ${cfData.url}`, + template: 'user-requesting-share', + fields: { + username: actor.type.user.username, + }, + }, + ); } else { - svc_notification.notify(UsernameNotifSelector(actor.type.user.username), - { - source: 'worker', - title: `Failed to deploy ${workerName}! ${cfData.errors}`, - template: 'user-requesting-share', - fields: { - username: actor.type.user.username, - }, - }); + svc_notification.notify( + UsernameNotifSelector(actor.type.user.username), + { + source: 'worker', + title: `Failed to deploy ${workerName}! ${cfData.errors}`, + template: 'user-requesting-share', + fields: { + username: actor.type.user.username, + }, + }, + ); } } catch (e) { - svc_notification.notify(UsernameNotifSelector(actor.type.user.username), - { - source: 'worker', - title: `Failed to deploy ${workerName}!!\n ${e}`, - template: 'user-requesting-share', - fields: { - username: actor.type.user.username, - }, - }); + svc_notification.notify( + UsernameNotifSelector(actor.type.user.username), + { + source: 'worker', + title: `Failed to deploy ${workerName}!!\n ${e}`, + template: 'user-requesting-share', + fields: { + username: actor.type.user.username, + }, + }, + ); } } }); diff --git a/src/backend/src/util/expressutil.js b/src/backend/src/util/expressutil.js index 26488751b7..e6ca4a5902 100644 --- a/src/backend/src/util/expressutil.js +++ b/src/backend/src/util/expressutil.js @@ -18,6 +18,12 @@ */ const eggspress = require('../api/eggspress'); +/** + * @deprecated Use eggspress directly + * @param {any} spec + * @param {any} handler + * @returns {any} + */ const Endpoint = function Endpoint (spec, handler) { return { attach (route) { @@ -29,9 +35,11 @@ const Endpoint = function Endpoint (spec, handler) { ...(spec.mw ? { mw: spec.mw } : {}), ...spec.otherOpts, }; - const eggspress_router = eggspress(spec.route, - eggspress_options, - handler ?? spec.handler); + const eggspress_router = eggspress( + spec.route, + eggspress_options, + handler ?? spec.handler, + ); route.use(eggspress_router); }, but (newSpec) { From 68f17e67fc0dd90f5815f30581e2d53ff2137ab7 Mon Sep 17 00:00:00 2001 From: ProgrammerIn-wonderland <3838shah@gmail.com> Date: Thu, 26 Mar 2026 03:46:21 -0400 Subject: [PATCH 2/3] Migrate user-protected endpoints to eggspress --- .../routers/user-protected/change-email.js | 177 +++++++++--------- .../routers/user-protected/change-password.js | 65 ++++--- .../routers/user-protected/change-username.js | 97 +++++----- .../routers/user-protected/delete-own-user.js | 23 ++- .../src/routers/user-protected/disable-2fa.js | 39 ++-- 5 files changed, 198 insertions(+), 203 deletions(-) diff --git a/src/backend/src/routers/user-protected/change-email.js b/src/backend/src/routers/user-protected/change-email.js index 20c104fb9c..f04856cd7a 100644 --- a/src/backend/src/routers/user-protected/change-email.js +++ b/src/backend/src/routers/user-protected/change-email.js @@ -15,7 +15,8 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - */ +*/ +const eggspress = require('../../api/eggspress'); const APIError = require('../../api/APIError'); const { DB_WRITE } = require('../../services/database/consts'); const jwt = require('jsonwebtoken'); @@ -26,104 +27,102 @@ const { Context } = require('../../util/context'); const { v4: uuidv4 } = require('uuid'); const { invalidate_cached_user_by_id } = require('../../helpers'); -module.exports = { - route: '/change-email', - methods: ['POST'], - handler: async (req, res) => { - const user = req.user; - const new_email = req.body.new_email; +module.exports = eggspress('/change-email', { + allowedMethods: ['POST'], +}, async (req, res) => { + const user = req.user; + const new_email = req.body.new_email; - // TODO: DRY: signup.js - // validation - if ( ! new_email ) { - throw APIError.create('field_missing', null, { key: 'new_email' }); - } - if ( typeof new_email !== 'string' ) { - throw APIError.create('field_invalid', null, { - key: 'new_email', expected: 'a valid email address' }); - } - if ( ! validator.isEmail(new_email) ) { - throw APIError.create('field_invalid', null, { - key: 'new_email', expected: 'a valid email address' }); - } + // TODO: DRY: signup.js + // validation + if ( ! new_email ) { + throw APIError.create('field_missing', null, { key: 'new_email' }); + } + if ( typeof new_email !== 'string' ) { + throw APIError.create('field_invalid', null, { + key: 'new_email', expected: 'a valid email address' }); + } + if ( ! validator.isEmail(new_email) ) { + throw APIError.create('field_invalid', null, { + key: 'new_email', expected: 'a valid email address' }); + } - const svc_cleanEmail = req.services.get('clean-email'); - const clean_email = svc_cleanEmail.clean(new_email); + const svc_cleanEmail = req.services.get('clean-email'); + const clean_email = svc_cleanEmail.clean(new_email); - if ( ! await svc_cleanEmail.validate(clean_email) ) { - throw APIError.create('email_not_allowed', undefined, { - email: clean_email, - }); - } + if ( ! await svc_cleanEmail.validate(clean_email) ) { + throw APIError.create('email_not_allowed', undefined, { + email: clean_email, + }); + } - // check if email is already in use - const db = req.services.get('database').get(DB_WRITE, 'auth'); - const rows = await db.read( - 'SELECT COUNT(*) AS `count` FROM `user` WHERE (`email` = ? OR `clean_email` = ?) AND `email_confirmed` = 1', - [new_email, clean_email], - ); + // check if email is already in use + const db = req.services.get('database').get(DB_WRITE, 'auth'); + const rows = await db.read( + 'SELECT COUNT(*) AS `count` FROM `user` WHERE (`email` = ? OR `clean_email` = ?) AND `email_confirmed` = 1', + [new_email, clean_email], + ); - // TODO: DRY: signup.js, save_account.js - if ( rows[0].count > 0 ) { - throw APIError.create('email_already_in_use', null, { email: new_email }); - } + // TODO: DRY: signup.js, save_account.js + if ( rows[0].count > 0 ) { + throw APIError.create('email_already_in_use', null, { email: new_email }); + } - // If user does not have a confirmed email, then update `email` directly - // and send a new confirmation email for their account instead. - if ( ! user.email_confirmed ) { - const email_confirm_token = uuidv4(); - await db.write( - 'UPDATE `user` SET `email` = ?, `email_confirm_token` = ? WHERE `id` = ?', - [new_email, email_confirm_token, user.id], - ); - invalidate_cached_user_by_id(user.id); + // If user does not have a confirmed email, then update `email` directly + // and send a new confirmation email for their account instead. + if ( ! user.email_confirmed ) { + const email_confirm_token = uuidv4(); + await db.write( + 'UPDATE `user` SET `email` = ?, `email_confirm_token` = ? WHERE `id` = ?', + [new_email, email_confirm_token, user.id], + ); + invalidate_cached_user_by_id(user.id); - const svc_email = Context.get('services').get('email'); - const link = `${config.origin}/confirm-email-by-token?user_uuid=${user.uuid}&token=${email_confirm_token}`; - svc_email.send_email({ email: new_email }, 'email_verification_link', { link }); + const svc_email = Context.get('services').get('email'); + const link = `${config.origin}/confirm-email-by-token?user_uuid=${user.uuid}&token=${email_confirm_token}`; + svc_email.send_email({ email: new_email }, 'email_verification_link', { link }); - res.send({ success: true }); - return; - } + res.send({ success: true }); + return; + } - // generate confirmation token - const token = crypto.randomBytes(4).toString('hex'); - const jwt_token = jwt.sign({ - user_id: user.id, - token, - }, config.jwt_secret, { expiresIn: '24h' }); + // generate confirmation token + const token = crypto.randomBytes(4).toString('hex'); + const jwt_token = jwt.sign({ + user_id: user.id, + token, + }, config.jwt_secret, { expiresIn: '24h' }); - // send confirmation email - const svc_email = req.services.get('email'); - await svc_email.send_email({ email: new_email }, 'email_change_request', { - confirm_url: `${config.origin}/change_email/confirm?token=${jwt_token}`, - username: user.username, - }); - const old_email = user.email; - // TODO: NotificationService - await svc_email.send_email({ email: old_email }, 'email_change_notification', { - new_email: new_email, - }); + // send confirmation email + const svc_email = req.services.get('email'); + await svc_email.send_email({ email: new_email }, 'email_change_request', { + confirm_url: `${config.origin}/change_email/confirm?token=${jwt_token}`, + username: user.username, + }); + const old_email = user.email; + // TODO: NotificationService + await svc_email.send_email({ email: old_email }, 'email_change_notification', { + new_email: new_email, + }); - // update user - await db.write( - 'UPDATE `user` SET `unconfirmed_change_email` = ?, `change_email_confirm_token` = ? WHERE `id` = ?', - [new_email, token, user.id], - ); - invalidate_cached_user_by_id(user.id); + // update user + await db.write( + 'UPDATE `user` SET `unconfirmed_change_email` = ?, `change_email_confirm_token` = ? WHERE `id` = ?', + [new_email, token, user.id], + ); + invalidate_cached_user_by_id(user.id); - // Update email change audit table - await db.write( - 'INSERT INTO `user_update_audit` ' + - '(`user_id`, `user_id_keep`, `old_email`, `new_email`, `reason`) ' + - 'VALUES (?, ?, ?, ?, ?)', - [ - req.user.id, req.user.id, - old_email, new_email, - 'change_username', - ], - ); + // Update email change audit table + await db.write( + 'INSERT INTO `user_update_audit` ' + + '(`user_id`, `user_id_keep`, `old_email`, `new_email`, `reason`) ' + + 'VALUES (?, ?, ?, ?, ?)', + [ + req.user.id, req.user.id, + old_email, new_email, + 'change_username', + ], + ); - res.send({ success: true }); - }, -}; + res.send({ success: true }); +}); diff --git a/src/backend/src/routers/user-protected/change-password.js b/src/backend/src/routers/user-protected/change-password.js index f33b512227..25ebc12905 100644 --- a/src/backend/src/routers/user-protected/change-password.js +++ b/src/backend/src/routers/user-protected/change-password.js @@ -18,6 +18,7 @@ */ // TODO: DRY: This is the same function used by UIWindowChangePassword! +const eggspress = require('../../api/eggspress'); const { invalidate_cached_user } = require('../../helpers'); const { DB_WRITE } = require('../../services/database/consts'); @@ -72,40 +73,38 @@ const check_password_strength = (password) => { }; }; -module.exports = { - route: '/change-password', - methods: ['POST'], - handler: async (req, res) => { - // Validate new password - const { new_pass } = req.body; - const { overallPass: strong } = check_password_strength(new_pass); - if ( ! strong ) { - req.status(400).send('Password does not meet requirements.'); - } +module.exports = eggspress('/change-password', { + allowedMethods: ['POST'], +}, async (req, res) => { + // Validate new password + const { new_pass } = req.body; + const { overallPass: strong } = check_password_strength(new_pass); + if ( ! strong ) { + req.status(400).send('Password does not meet requirements.'); + } - // Update user - // TODO: DI for endpoint definitions like this one - const bcrypt = require('bcrypt'); - const db = req.services.get('database').get(DB_WRITE, 'auth'); - await db.write( - 'UPDATE user SET password=?, `pass_recovery_token` = NULL, `change_email_confirm_token` = NULL WHERE `id` = ?', - [await bcrypt.hash(req.body.new_pass, 8), req.user.id], - ); - invalidate_cached_user(req.user); + // Update user + // TODO: DI for endpoint definitions like this one + const bcrypt = require('bcrypt'); + const db = req.services.get('database').get(DB_WRITE, 'auth'); + await db.write( + 'UPDATE user SET password=?, `pass_recovery_token` = NULL, `change_email_confirm_token` = NULL WHERE `id` = ?', + [await bcrypt.hash(req.body.new_pass, 8), req.user.id], + ); + invalidate_cached_user(req.user); - // Notify user about password change - // TODO: audit log for user in security tab - const svc_email = req.services.get('email'); - svc_email.send_email({ email: req.user.email }, 'password_change_notification'); + // Notify user about password change + // TODO: audit log for user in security tab + const svc_email = req.services.get('email'); + svc_email.send_email({ email: req.user.email }, 'password_change_notification'); - // Kick out all other sessions - const svc_auth = req.services.get('auth'); - const sessions = await svc_auth.list_sessions(req.actor); - for ( const session of sessions ) { - if ( session.current ) continue; - await svc_auth.revoke_session(req.actor, session.uuid); - } + // Kick out all other sessions + const svc_auth = req.services.get('auth'); + const sessions = await svc_auth.list_sessions(req.actor); + for ( const session of sessions ) { + if ( session.current ) continue; + await svc_auth.revoke_session(req.actor, session.uuid); + } - return res.send('Password successfully updated.'); - }, -}; + return res.send('Password successfully updated.'); +}); diff --git a/src/backend/src/routers/user-protected/change-username.js b/src/backend/src/routers/user-protected/change-username.js index 6917974d83..55ab57a3fc 100644 --- a/src/backend/src/routers/user-protected/change-username.js +++ b/src/backend/src/routers/user-protected/change-username.js @@ -15,65 +15,64 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - */ +*/ +const eggspress = require('../../api/eggspress'); const config = require('../../config'); const APIError = require('../../api/APIError.js'); const { DB_WRITE } = require('../../services/database/consts'); const { username_exists, change_username } = require('../../helpers'); const { Context } = require('../../util/context'); -module.exports = { - route: '/change-username', - methods: ['POST'], - handler: async (req, res, _next) => { - const user = req.user; - const new_username = req.body.new_username; +module.exports = eggspress('/change-username', { + allowedMethods: ['POST'], +}, async (req, res, _next) => { + const user = req.user; + const new_username = req.body.new_username; - if ( ! new_username ) { - throw APIError.create('field_missing', null, { key: 'new_username' }); - } - if ( typeof new_username !== 'string' ) { - throw APIError.create('field_invalid', null, { key: 'new_username', expected: 'a string' }); - } - if ( ! new_username.match(config.username_regex) ) { - throw APIError.create('field_invalid', null, { key: 'new_username', expected: 'letters, numbers, underscore (_)' }); - } - if ( new_username.length > config.username_max_length ) { - throw APIError.create('field_too_long', null, { key: 'new_username', max_length: config.username_max_length }); - } - if ( await username_exists(new_username) ) { - throw APIError.create('username_already_in_use', null, { username: new_username }); - } + if ( ! new_username ) { + throw APIError.create('field_missing', null, { key: 'new_username' }); + } + if ( typeof new_username !== 'string' ) { + throw APIError.create('field_invalid', null, { key: 'new_username', expected: 'a string' }); + } + if ( ! new_username.match(config.username_regex) ) { + throw APIError.create('field_invalid', null, { key: 'new_username', expected: 'letters, numbers, underscore (_)' }); + } + if ( new_username.length > config.username_max_length ) { + throw APIError.create('field_too_long', null, { key: 'new_username', max_length: config.username_max_length }); + } + if ( await username_exists(new_username) ) { + throw APIError.create('username_already_in_use', null, { username: new_username }); + } - const svc_edgeRateLimit = req.services.get('edge-rate-limit'); - if ( ! svc_edgeRateLimit.check('/user-protected/change-username') ) { - return res.status(429).send('Too many requests.'); - } + const svc_edgeRateLimit = req.services.get('edge-rate-limit'); + if ( ! svc_edgeRateLimit.check('/user-protected/change-username') ) { + return res.status(429).send('Too many requests.'); + } - const db = Context.get('services').get('database').get(DB_WRITE, 'auth'); - const rows = await db.read( - 'SELECT COUNT(*) AS `count` FROM `user_update_audit` ' + - `WHERE \`user_id\`=? AND \`reason\`=? AND ${ - db.case({ - mysql: '`created_at` > DATE_SUB(NOW(), INTERVAL 1 MONTH)', - sqlite: "`created_at` > datetime('now', '-1 month')", - })}`, - [user.id, 'change_username'], - ); + const db = Context.get('services').get('database').get(DB_WRITE, 'auth'); + const rows = await db.read( + 'SELECT COUNT(*) AS `count` FROM `user_update_audit` ' + + `WHERE \`user_id\`=? AND \`reason\`=? AND ${ + db.case({ + mysql: '`created_at` > DATE_SUB(NOW(), INTERVAL 1 MONTH)', + sqlite: "`created_at` > datetime('now', '-1 month')", + })}`, + [user.id, 'change_username'], + ); - if ( rows[0].count >= (config.max_username_changes ?? 2) ) { - throw APIError.create('too_many_username_changes'); - } + if ( rows[0].count >= (config.max_username_changes ?? 2) ) { + throw APIError.create('too_many_username_changes'); + } - await db.write( - 'INSERT INTO `user_update_audit` ' + - '(`user_id`, `user_id_keep`, `old_username`, `new_username`, `reason`) ' + - 'VALUES (?, ?, ?, ?, ?)', - [user.id, user.id, user.username, new_username, 'change_username'], - ); + await db.write( + 'INSERT INTO `user_update_audit` ' + + '(`user_id`, `user_id_keep`, `old_username`, `new_username`, `reason`) ' + + 'VALUES (?, ?, ?, ?, ?)', + [user.id, user.id, user.username, new_username, 'change_username'], + ); - await change_username(user.id, new_username); + await change_username(user.id, new_username); - res.json({}); - }, -}; + res.json({}); +}); diff --git a/src/backend/src/routers/user-protected/delete-own-user.js b/src/backend/src/routers/user-protected/delete-own-user.js index 470ebdd3a8..9b3e483d46 100644 --- a/src/backend/src/routers/user-protected/delete-own-user.js +++ b/src/backend/src/routers/user-protected/delete-own-user.js @@ -15,22 +15,21 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - */ +*/ +const eggspress = require('../../api/eggspress'); const config = require('../../config'); const { deleteUser, invalidate_cached_user } = require('../../helpers'); const REVALIDATION_COOKIE_NAME = 'puter_revalidation'; -module.exports = { - route: '/delete-own-user', - methods: ['POST'], - handler: async (req, res) => { - res.clearCookie(config.cookie_name); - res.clearCookie(REVALIDATION_COOKIE_NAME); +module.exports = eggspress('/delete-own-user', { + allowedMethods: ['POST'], +}, async (req, res) => { + res.clearCookie(config.cookie_name); + res.clearCookie(REVALIDATION_COOKIE_NAME); - await deleteUser(req.user.id); - invalidate_cached_user(req.user); + await deleteUser(req.user.id); + invalidate_cached_user(req.user); - return res.send({ success: true }); - }, -}; + return res.send({ success: true }); +}); diff --git a/src/backend/src/routers/user-protected/disable-2fa.js b/src/backend/src/routers/user-protected/disable-2fa.js index 175c72a9ee..dbcd833198 100644 --- a/src/backend/src/routers/user-protected/disable-2fa.js +++ b/src/backend/src/routers/user-protected/disable-2fa.js @@ -15,28 +15,27 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . - */ +*/ +const eggspress = require('../../api/eggspress'); const { DB_WRITE } = require('../../services/database/consts'); const { invalidate_cached_user_by_id } = require('../../helpers'); -module.exports = { - route: '/disable-2fa', - methods: ['POST'], - handler: async (req, res) => { - const db = req.services.get('database').get(DB_WRITE, '2fa.disable'); - await db.write( - 'UPDATE user SET otp_enabled = 0, otp_recovery_codes = NULL, otp_secret = NULL WHERE uuid = ?', - [req.user.uuid], - ); - // update cached user - req.user.otp_enabled = 0; - invalidate_cached_user_by_id(req.user.id); +module.exports = eggspress('/disable-2fa', { + allowedMethods: ['POST'], +}, async (req, res) => { + const db = req.services.get('database').get(DB_WRITE, '2fa.disable'); + await db.write( + 'UPDATE user SET otp_enabled = 0, otp_recovery_codes = NULL, otp_secret = NULL WHERE uuid = ?', + [req.user.uuid], + ); + // update cached user + req.user.otp_enabled = 0; + invalidate_cached_user_by_id(req.user.id); - const svc_email = req.services.get('email'); - await svc_email.send_email({ email: req.user.email }, 'disabled_2fa', { - username: req.user.username, - }); + const svc_email = req.services.get('email'); + await svc_email.send_email({ email: req.user.email }, 'disabled_2fa', { + username: req.user.username, + }); - res.send({ success: true }); - }, -}; + res.send({ success: true }); +}); From c7de6bf6f5beb1a565cbf5ba0c3d6e910797b66d Mon Sep 17 00:00:00 2001 From: ProgrammerIn-wonderland <3838shah@gmail.com> Date: Thu, 26 Mar 2026 04:04:12 -0400 Subject: [PATCH 3/3] eggspress better typedefs --- src/backend/src/modules/web/lib/eggspress.js | 52 ++++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/backend/src/modules/web/lib/eggspress.js b/src/backend/src/modules/web/lib/eggspress.js index d80c565222..dadd0df819 100644 --- a/src/backend/src/modules/web/lib/eggspress.js +++ b/src/backend/src/modules/web/lib/eggspress.js @@ -27,18 +27,50 @@ const { Context } = require('../../../util/context.js'); const { subdomain } = require('../../../helpers.js'); const config = require('../../../config.js'); +// Oneday, this will be a typescript typedef. +// Doesn't seem like that day is today. + +/** + * @typedef {"GET"|"HEAD"|"POST"|"PUT"|"DELETE"|"PROPFIND"|"PROPPATCH"|"MKCOL"|"COPY"|"MOVE"|"LOCK"|"UNLOCK"|"OPTIONS"} EggspressMethod + */ + +/** + * @typedef {{ + * consolidate(args: { + * req: import('express').Request, + * getParam: (key: string) => unknown, + * }): Promise|unknown, + * }} EggspressParamDefinition + */ + +/** + * @typedef {object} EggspressSettings + * @property {boolean} [auth] + * @property {boolean} [auth2] + * @property {unknown} [abuse] + * @property {boolean} [verified] + * @property {boolean} [json] + * @property {boolean} [jsonCanBeLarge] + * @property {boolean} [noReallyItsJson] + * @property {string[]} [files] + * @property {boolean} [multest] + * @property {string[]} [multipart_jsons] + * @property {Record} [alias] + * @property {Record} [parameters] + * @property {import('express').RequestHandler} [customArgs] + * @property {number} [alarm_timeout] + * @property {number} [response_timeout] + * @property {import('express').RequestHandler[]} [mw] + * @property {EggspressMethod[]} allowedMethods + * @property {string} [subdomain] + */ + /** * eggspress() is a factory function for creating express routers. * - * @param {*} route the route to the router - * @param {*} settings the settings for the router. The following - * properties are supported: - * - auth: whether or not to use the auth middleware - * - fs: whether or not to use the fs middleware - * - json: whether or not to use the json middleware - * - customArgs: custom arguments to pass to the router - * - allowedMethods: the allowed HTTP methods - * @param {*} handler the handler for the router + * @param {string|RegExp|(string|RegExp)[]} route the route to the router + * @param {EggspressSettings} settings the settings for the router + * @param {import('express').RequestHandler} handler the handler for the router * @returns {express.Router} the router */ module.exports = function eggspress (route, settings, handler) { @@ -278,4 +310,4 @@ module.exports = function eggspress (route, settings, handler) { } return router; -}; \ No newline at end of file +};