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
+};