From 603746951b8da41ccce9a5b9301e5dcc1e8b1508 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Fri, 31 Oct 2025 15:48:21 -0700 Subject: [PATCH] feat: ui for dev account + util decorators for controllers in extension (#1864) * fix: IPC trigger for dev account setup + util decorators for ext controllers * feat: allow extension to bypass json * fix: ts building in volatile --- doc/contributors/extensions/README.md | 2 +- extensions/ExtensionController/.gitignore | 2 + extensions/ExtensionController/package.json | 22 + .../src/ExtensionController.ts | 71 + extensions/ExtensionController/src/index.ts | 4 + extensions/ExtensionController/tsconfig.json | 39 + extensions/api.d.ts | 17 +- extensions/whoami/routes.js | 38 +- src/backend/src/Extension.js | 164 +- src/backend/src/ExtensionService.js | 55 +- src/backend/src/modules/web/lib/eggspress.js | 61 +- src/backend/src/routers/signup.js | 6 +- src/backend/src/services/GetUserService.js | 46 +- .../MeteringService/subPolicies/index.ts | 4 +- .../subPolicies/registeredUserFreePolicy.ts | 2 +- .../subPolicies/tempUserFreePolicy.ts | 2 +- src/backend/src/services/User.d.ts | 8 + src/backend/src/services/UserService.d.ts | 12 + src/backend/src/services/UserService.js | 107 +- src/backend/src/services/auth/Actor.d.ts | 10 +- .../database/BaseDatabaseAccessService.d.ts | 29 + .../sqlite_setup/0040_add_user_metadata.sql | 1 + src/backend/src/util/expressutil.js | 21 +- src/dev-center/index.html | 565 +++--- src/dev-center/js/apps.js | 1533 +++++++++-------- tools/api-tester/config.yml | 8 + tsconfig.json | 5 +- 27 files changed, 1642 insertions(+), 1192 deletions(-) create mode 100644 extensions/ExtensionController/.gitignore create mode 100644 extensions/ExtensionController/package.json create mode 100644 extensions/ExtensionController/src/ExtensionController.ts create mode 100644 extensions/ExtensionController/src/index.ts create mode 100644 extensions/ExtensionController/tsconfig.json create mode 100644 src/backend/src/services/User.d.ts create mode 100644 src/backend/src/services/UserService.d.ts create mode 100644 src/backend/src/services/database/BaseDatabaseAccessService.d.ts create mode 100644 src/backend/src/services/database/sqlite_setup/0040_add_user_metadata.sql create mode 100644 tools/api-tester/config.yml diff --git a/doc/contributors/extensions/README.md b/doc/contributors/extensions/README.md index be00793fc..0af053c18 100644 --- a/doc/contributors/extensions/README.md +++ b/doc/contributors/extensions/README.md @@ -67,7 +67,7 @@ in order to access `db` from callbacks. ```javascript const ext = extension; -extension.get('/user-count', { noauth: true }, (req, res) => { +extension.get('/user-count', { noauth: true, mw: [] }, (req, res) => { const [count] = await ext.db.read( 'SELECT COUNT(*) as c FROM `user`' ); diff --git a/extensions/ExtensionController/.gitignore b/extensions/ExtensionController/.gitignore new file mode 100644 index 000000000..69a24fb74 --- /dev/null +++ b/extensions/ExtensionController/.gitignore @@ -0,0 +1,2 @@ +*.js +*.map \ No newline at end of file diff --git a/extensions/ExtensionController/package.json b/extensions/ExtensionController/package.json new file mode 100644 index 000000000..fda136aaf --- /dev/null +++ b/extensions/ExtensionController/package.json @@ -0,0 +1,22 @@ +{ + "name": "extensionController", + "priority": -1000, + "version": "1.0.0", + "description": "", + "main": "src/index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.9.1", + "ts-node": "^10.9.2" + }, + "dependencies": { + "stripe": "^19.1.0" + } +} \ No newline at end of file diff --git a/extensions/ExtensionController/src/ExtensionController.ts b/extensions/ExtensionController/src/ExtensionController.ts new file mode 100644 index 000000000..6a7aab6de --- /dev/null +++ b/extensions/ExtensionController/src/ExtensionController.ts @@ -0,0 +1,71 @@ +import type { Request, Response } from 'express'; +import type { EndpointOptions, HttpMethod } from '../../api.d.ts'; + +/** + * Controller decorator to set prefix on prototype and register routes on instantiation + */ +export const Controller = (prefix: string): ClassDecorator => { + return (target: Function) => { + target.prototype.__controllerPrefix = prefix; + }; +}; + +/** + * Method decorator factory that collects route metadata + */ +interface RouteMeta { + method: HttpMethod; + path: string; + options?: EndpointOptions | undefined; + handler: (req: Request, res: Response)=> void | Promise; +} + +const createMethodDecorator = (method: HttpMethod) => { + return (path: string, options?: EndpointOptions) => { + + return (target: (req: Request, res: Response)=> void | Promise, _context: ClassMethodDecoratorContext void | Promise>) => { + + _context.addInitializer(function() { + const proto = Object.getPrototypeOf(this); + if ( !proto.__routes ) { + proto.__routes = []; + } + proto.__routes.push({ + method, + path, + options: options as EndpointOptions | undefined, + handler: target, + }); + }); + + }; + }; +}; + +// HTTP method decorators +export const Get = createMethodDecorator('get'); +export const Post = createMethodDecorator('post'); +export const Put = createMethodDecorator('put'); +export const Delete = createMethodDecorator('delete'); +// TODO DS: add others as needed (patch, etc) + +// Registers all routes from a decorated controller instance to an Express router + +export class ExtensionController { + + // TODO DS: make this work with other express-like routers + registerRoutes() { + const prefix = Object.getPrototypeOf(this).__controllerPrefix || ''; + const routes: RouteMeta[] = Object.getPrototypeOf(this).__routes || []; + for ( const route of routes ) { + const fullPath = `${prefix}/${route.path}`.replace(/\/+/g, '/'); + if ( !extension[route.method] ){ + throw new Error(`Unsupported HTTP method: ${route.method}`); + } else { + console.log(`Registering route: [${route.method.toUpperCase()}] ${fullPath}`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (extension[route.method] as any)(fullPath, route.options, route.handler.bind(this)); + } + } + } +} diff --git a/extensions/ExtensionController/src/index.ts b/extensions/ExtensionController/src/index.ts new file mode 100644 index 000000000..479485377 --- /dev/null +++ b/extensions/ExtensionController/src/index.ts @@ -0,0 +1,4 @@ +//@puter priority -1000 +import * as extensionControllerExports from './ExtensionController.js'; + +extension.exports = { ...extensionControllerExports }; diff --git a/extensions/ExtensionController/tsconfig.json b/extensions/ExtensionController/tsconfig.json new file mode 100644 index 000000000..f1241ce1b --- /dev/null +++ b/extensions/ExtensionController/tsconfig.json @@ -0,0 +1,39 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + // "outDir": "./dist", + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} \ No newline at end of file diff --git a/extensions/api.d.ts b/extensions/api.d.ts index 1ad3fca4d..c303ff20a 100644 --- a/extensions/api.d.ts +++ b/extensions/api.d.ts @@ -1,17 +1,23 @@ import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js'; +import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts'; import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.ts'; import type { MeteringServiceWrapper } from '@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs'; import type { DBKVStore } from '@heyputer/backend/src/services/repositories/DBKVStore/DBKVStore.ts'; import type { SUService } from '@heyputer/backend/src/services/SUService.js'; +import type { IUser } from '@heyputer/backend/src/services/User.js'; +import type { UserService } from '@heyputer/backend/src/services/UserService.d.ts'; import type { RequestHandler } from 'express'; import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js'; import type helpers from '../src/backend/src/helpers.js'; +import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts'; declare global { namespace Express { interface Request { services: { get: (string: T)=> T extends keyof ServiceNameMap ? ServiceNameMap[T] : unknown } - actor: Actor + actor: Actor, + /** @deprecated use actor instead */ + user: IUser } } } @@ -20,6 +26,11 @@ interface EndpointOptions { allowedMethods?: string[] subdomain?: string noauth?: boolean + mw?: RequestHandler[] + otherOpts?: Record & { + json?: boolean + noReallyItsJson?: boolean + } } type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; @@ -50,10 +61,14 @@ interface ServiceNameMap { 'meteringService': Pick & MeteringService // TODO DS: squash into a single class without wrapper 'puter-kvstore': DBKVStore 'su': SUService + 'database': BaseDatabaseAccessService + 'user': UserService } interface Extension extends RouterMethods { + exports: Record, import(module:'core'): CoreRuntimeModule, import(module:'fs'): FilesystemModule, + import(module:'extensionController'): typeof ExtensionControllerExports import(module: T): T extends `service:${infer R extends keyof ServiceNameMap}` ? ServiceNameMap[R] : unknown; diff --git a/extensions/whoami/routes.js b/extensions/whoami/routes.js index 1697c378e..fe63cd332 100644 --- a/extensions/whoami/routes.js +++ b/extensions/whoami/routes.js @@ -47,7 +47,7 @@ const whoami_common = ({ is_user, user }) => { epoch = new Date(user.last_activity_ts).getTime(); // round to 1 decimal place epoch = Math.round(epoch / 1000); - } catch (e) { + } catch ( e ) { console.error('Error parsing last_activity_ts', e); } @@ -94,6 +94,7 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => { referral_code: req.user.referral_code, otp: !! req.user.otp_enabled, human_readable_age: timeago.format(new Date(req.user.timestamp)), + hasDevAccountAccess: !! req.user.metadata?.hasDevAccountAccess, ...(req.new_token ? { token: req.token } : {}), }; @@ -148,37 +149,35 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => { let desktop_items = []; // check if user asked for desktop items - if(req.query.return_desktop_items === 1 || req.query.return_desktop_items === '1' || req.query.return_desktop_items === 'true'){ + if ( req.query.return_desktop_items === 1 || req.query.return_desktop_items === '1' || req.query.return_desktop_items === 'true' ){ // by cached desktop id - if(req.user.desktop_id){ + if ( req.user.desktop_id ){ // TODO: Check if used anywhere, maybe remove // eslint-disable-next-line no-undef - desktop_items = await db.read( - `SELECT * FROM fsentries + desktop_items = await db.read(`SELECT * FROM fsentries WHERE user_id = ? AND parent_uid = ?`, - [req.user.id, await id2uuid(req.user.desktop_id)] - ) + [req.user.id, await id2uuid(req.user.desktop_id)]); } // by desktop path - else{ - desktop_items = await get_descendants(req.user.username +'/Desktop', req.user, 1, true); + else { + desktop_items = await get_descendants(req.user.username + '/Desktop', req.user, 1, true); } // clean up desktop items and add some extra information - if(desktop_items.length > 0){ - if(desktop_items.length > 0){ - for (let i = 0; i < desktop_items.length; i++) { - if(desktop_items[i].id !== null){ + if ( desktop_items.length > 0 ){ + if ( desktop_items.length > 0 ){ + for ( let i = 0; i < desktop_items.length; i++ ) { + if ( desktop_items[i].id !== null ){ // suggested_apps for files - if(!desktop_items[i].is_dir){ - desktop_items[i].suggested_apps = await suggest_app_for_fsentry(desktop_items[i], {user: req.user}); + if ( !desktop_items[i].is_dir ){ + desktop_items[i].suggested_apps = await suggest_app_for_fsentry(desktop_items[i], { user: req.user }); } // is_shared desktop_items[i].is_shared = await is_shared_with_anyone(desktop_items[i].id); // associated_app - if(desktop_items[i].associated_app_id){ - const app = await get_app({id: desktop_items[i].associated_app_id}) + if ( desktop_items[i].associated_app_id ){ + const app = await get_app({ id: desktop_items[i].associated_app_id }); // remove some privileged information delete app.id; @@ -189,7 +188,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => { // add to array desktop_items[i].associated_app = app; - }else{ + } else { desktop_items[i].associated_app = {}; } @@ -200,7 +199,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => { delete desktop_items[i].id; delete desktop_items[i].user_id; delete desktop_items[i].bucket; - desktop_items[i].path = _path.join('/', req.user.username, desktop_items[i].name) + desktop_items[i].path = _path.join('/', req.user.username, desktop_items[i].name); } } } @@ -221,5 +220,6 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => { taskbar_items: await get_taskbar_items(req.user), desktop_items: desktop_items, referral_code: req.user.referral_code, + hasDevAccountAccess: !! req.user.metadata?.hasDevAccountAccess, }, whoami_common({ is_user, user: req.user }))); }); diff --git a/src/backend/src/Extension.js b/src/backend/src/Extension.js index fad45b361..cb233e2ad 100644 --- a/src/backend/src/Extension.js +++ b/src/backend/src/Extension.js @@ -1,27 +1,27 @@ /* * 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 { AdvancedBase } = require("@heyputer/putility"); -const EmitterFeature = require("@heyputer/putility/src/features/EmitterFeature"); -const { Context } = require("./util/context"); -const { ExtensionServiceState } = require("./ExtensionService"); -const { display_time } = require("@heyputer/putility/src/libs/time"); +const { AdvancedBase } = require('@heyputer/putility'); +const EmitterFeature = require('@heyputer/putility/src/features/EmitterFeature'); +const { Context } = require('./util/context'); +const { ExtensionServiceState } = require('./ExtensionService'); +const { display_time } = require('@heyputer/putility/src/libs/time'); /** * This class creates the `extension` global that is seen by Puter backend @@ -33,11 +33,11 @@ class Extension extends AdvancedBase { decorators: [ fn => Context.get(undefined, { allow_fallback: true, - }).abind(fn) - ] + }).abind(fn), + ], }), ]; - + randomBrightColor() { // Bright colors in ANSI (foreground codes 90–97) const brightColors = [ @@ -52,30 +52,30 @@ class Extension extends AdvancedBase { return brightColors[Math.floor(Math.random() * brightColors.length)]; } - constructor (...a) { + constructor(...a) { super(...a); this.service = null; this.log = null; this.ensure_service_(); - + // this.terminal_color = this.randomBrightColor(); this.terminal_color = 94; - + this.log = (...a) => { this.log_context.info(a.join(' ')); }; this.LOG = (...a) => { this.log_context.noticeme(a.join(' ')); }; - ['info','warn','debug','error','tick','noticeme','system'].forEach(lvl => { + ['info', 'warn', 'debug', 'error', 'tick', 'noticeme', 'system'].forEach(lvl => { this.log[lvl] = (...a) => { this.log_context[lvl](...a); - } + }; }); - + this.only_one_preinit_fn = null; this.only_one_init_fn = null; - + this.registry = { register: this.register.bind(this), of: (typeKey) => { @@ -90,23 +90,23 @@ class Extension extends AdvancedBase { ...Object.values(this.registry_[typeKey].named), ...this.registry_[typeKey].anonymous, ], - } - } + }; + }, }; } - example () { + example() { console.log('Example method called by an extension.'); } - + // === [START] RuntimeModule aliases === - set exports (value) { + set exports(value) { this.runtime.exports = value; } - get exports () { + get exports() { return this.runtime.exports; } - import (name) { + import(name) { return this.runtime.import(name); } // === [END] RuntimeModule aliases === @@ -114,59 +114,53 @@ class Extension extends AdvancedBase { /** * This will get a database instance from the default service. */ - get db () { + get db() { const db = this.service.values.get('db'); if ( ! db ) { - throw new Error( - 'extension tried to access database before it was ' + - 'initialized' - ); + throw new Error('extension tried to access database before it was ' + + 'initialized'); } return db; } - get services () { + get services() { const services = this.service.values.get('services'); if ( ! services ) { - throw new Error( - 'extension tried to access "services" before it was ' + - 'initialized' - ); + throw new Error('extension tried to access "services" before it was ' + + 'initialized'); } return services; } - get log_context () { + get log_context() { const log_context = this.service.values.get('log_context'); if ( ! log_context ) { - throw new Error( - 'extension tried to access "log_context" before it was ' + - 'initialized' - ); + throw new Error('extension tried to access "log_context" before it was ' + + 'initialized'); } return log_context; } - + /** * Register anonymous or named data to a particular type/category. * @param {string} typeKey Type of data being registered * @param {string} [key] Key of data being registered * @param {any} data The data to be registered */ - register (typeKey, keyOrData, data) { + register(typeKey, keyOrData, data) { if ( ! this.registry_[typeKey] ) { this.registry_[typeKey] = { named: {}, anonymous: [], }; } - + const typeRegistry = this.registry_[typeKey]; - + if ( arguments.length <= 1 ) { throw new Error('you must specify what to register'); } - + if ( arguments.length === 2 ) { data = keyOrData; if ( Array.isArray(data) ) { @@ -178,28 +172,28 @@ class Extension extends AdvancedBase { typeRegistry.anonymous.push(data); return; } - + const key = keyOrData; typeRegistry.named[key] = data; } - + /** * Alias for .register() * @param {string} typeKey Type of data being registered * @param {string} [key] Key of data being registered * @param {any} data The data to be registered */ - reg (...a) { + reg(...a) { this.register(...a); } - + /** * This will create a GET endpoint on the default service. * @param {*} path - route for the endpoint * @param {*} handler - function to handle the endpoint * @param {*} options - options like noauth (bool) and mw (array) */ - get (path, handler, options) { + get(path, handler, options) { // this extension will have a default service this.ensure_service_(); @@ -221,7 +215,7 @@ class Extension extends AdvancedBase { * @param {*} handler - function to handle the endpoint * @param {*} options - options like noauth (bool) and mw (array) */ - post (path, handler, options) { + post(path, handler, options) { // this extension will have a default service this.ensure_service_(); @@ -236,8 +230,52 @@ class Extension extends AdvancedBase { methods: ['POST'], }); } - - use (...args) { + + /** + * This will create a DELETE endpoint on the default service. + * @param {*} path - route for the endpoint + * @param {*} handler - function to handle the endpoint + * @param {*} options - options like noauth (bool) and mw (array) + */ + put(path, handler, options) { + // this extension will have a default service + this.ensure_service_(); + + // handler and options may be flipped + if ( typeof handler === 'object' ) { + [handler, options] = [options, handler]; + } + if ( ! options ) options = {}; + + this.service.register_route_handler_(path, handler, { + ...options, + methods: ['PUT'], + }); + } + /** + * This will create a DELETE endpoint on the default service. + * @param {*} path - route for the endpoint + * @param {*} handler - function to handle the endpoint + * @param {*} options - options like noauth (bool) and mw (array) + */ + + delete(path, handler, options) { + // this extension will have a default service + this.ensure_service_(); + + // handler and options may be flipped + if ( typeof handler === 'object' ) { + [handler, options] = [options, handler]; + } + if ( ! options ) options = {}; + + this.service.register_route_handler_(path, handler, { + ...options, + methods: ['DELETE'], + }); + } + + use(...args) { this.ensure_service_(); this.service.expressThings_.push({ type: 'router', @@ -246,7 +284,7 @@ class Extension extends AdvancedBase { } get preinit() { - return (function (callback) { + return (function(callback) { this.on('preinit', callback); }).bind(this); } @@ -257,7 +295,8 @@ class Extension extends AdvancedBase { }); } if ( callback === null ) { - this.only_one_preinit_fn = () => {}; + this.only_one_preinit_fn = () => { + }; } this.only_one_preinit_fn = callback; } @@ -267,19 +306,20 @@ class Extension extends AdvancedBase { this.on('init', callback); }).bind(this); } - set init (callback) { + set init(callback) { if ( this.only_one_init_fn === null ) { this.on('init', (...a) => { this.only_one_init_fn(...a); }); } if ( callback === null ) { - this.only_one_init_fn = () => {}; + this.only_one_init_fn = () => { + }; } this.only_one_init_fn = callback; } - get console () { + get console() { const extensionConsole = Object.create(console); const logfn = level => (...a) => { let svc_log; @@ -317,10 +357,10 @@ class Extension extends AdvancedBase { * This method will create the "default service" for an extension. * This is specifically for Puter extensions that do not define their * own service classes. - * + * * @returns {void} */ - ensure_service_ () { + ensure_service_() { if ( this.service ) { return; } @@ -333,4 +373,4 @@ class Extension extends AdvancedBase { module.exports = { Extension, -} +}; diff --git a/src/backend/src/ExtensionService.js b/src/backend/src/ExtensionService.js index 8a3000661..d08927158 100644 --- a/src/backend/src/ExtensionService.js +++ b/src/backend/src/ExtensionService.js @@ -1,29 +1,29 @@ /* * 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 { AdvancedBase } = require("@heyputer/putility"); -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"); -const { Actor } = require("./services/auth/Actor"); +const { AdvancedBase } = require('@heyputer/putility'); +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'); +const { Actor } = require('./services/auth/Actor'); /** * State shared with the default service and the `extension` global so that @@ -31,17 +31,17 @@ const { Actor } = require("./services/auth/Actor"); * future) to the default service. */ class ExtensionServiceState extends AdvancedBase { - constructor (...a) { + constructor(...a) { super(...a); this.extension = a[0].extension; this.expressThings_ = []; - + // Values shared between the `extension` global and its service this.values = new Context(); } - register_route_handler_ (path, handler, options = {}) { + register_route_handler_(path, handler, options = {}) { // handler and options may be flipped if ( typeof handler === 'object' ) { [handler, options] = [options, handler]; @@ -64,8 +64,9 @@ class ExtensionServiceState extends AdvancedBase { route: path, handler: handler, ...(options.subdomain ? { subdomain: options.subdomain } : {}), + otherOpts: options.otherOpts || {}, }); - + this.expressThings_.push({ type: 'endpoint', value: endpoint }); } } @@ -76,15 +77,15 @@ class ExtensionServiceState extends AdvancedBase { * provide a default service for extensions. */ class ExtensionService extends BaseService { - _construct () { + _construct() { this.expressThings_ = []; } - async _init (args) { + async _init(args) { this.state = args.state; - + this.state.values.set('services', this.services); this.state.values.set('log_context', this.services.get('log-service').create( - this.state.extension.name)); + this.state.extension.name)); // Create database access object for extension const db = this.services.get('database').get(DB_WRITE, 'extension'); @@ -113,20 +114,20 @@ class ExtensionService extends BaseService { // Propagate all events from extension to Puter's event bus this.state.extension.on_all(async (key, data, meta) => { if ( meta.from_outside_of_extension ) return; - + await svc_event.emit(key, data, meta); }); - + this.state.extension.kv = (() => { const impls = this.services.get_implementors('puter-kvstore'); const impl_kv = impls[0].impl; - + return new Proxy(impl_kv, { get: (target, prop) => { if ( typeof target[prop] !== 'function' ) { return target[prop]; } - + return (...args) => { if ( typeof args[0] !== 'object' ) { // Luckily named parameters don't have positional @@ -152,7 +153,7 @@ class ExtensionService extends BaseService { this.state.extension.emit('preinit'); } - async ['__on_boot.consolidation'] (...a) { + async ['__on_boot.consolidation'](...a) { const svc_su = this.services.get('su'); await svc_su.sudo(async () => { await this.state.extension.emit('init', {}, { @@ -160,7 +161,7 @@ class ExtensionService extends BaseService { }); }); } - async ['__on_boot.activation'] (...a) { + async ['__on_boot.activation'](...a) { const svc_su = this.services.get('su'); await svc_su.sudo(async () => { await this.state.extension.emit('activate', {}, { @@ -168,7 +169,7 @@ class ExtensionService extends BaseService { }); }); } - async ['__on_boot.ready'] (...a) { + async ['__on_boot.ready'](...a) { const svc_su = this.services.get('su'); await svc_su.sudo(async () => { await this.state.extension.emit('ready', {}, { @@ -177,7 +178,7 @@ class ExtensionService extends BaseService { }); } - ['__on_install.routes'] (_, { app }) { + ['__on_install.routes'](_, { app }) { if ( ! this.state ) debugger; for ( const thing of this.state.expressThings_ ) { if ( thing.type === 'endpoint' ) { diff --git a/src/backend/src/modules/web/lib/eggspress.js b/src/backend/src/modules/web/lib/eggspress.js index 49ae0571e..b882c2171 100644 --- a/src/backend/src/modules/web/lib/eggspress.js +++ b/src/backend/src/modules/web/lib/eggspress.js @@ -43,28 +43,25 @@ const config = require('../../../config.js'); * @param {*} handler the handler for the router * @returns {express.Router} the router */ -module.exports = function eggspress (route, settings, handler) { +module.exports = function eggspress(route, settings, handler) { const router = express.Router(); const mw = []; const afterMW = []; - + const _defaultJsonOptions = {}; if ( settings.jsonCanBeLarge ) { _defaultJsonOptions.limit = '10mb'; } + const shouldJson = settings.json === undefined && settings.noReallyItsJson === undefined ? true : + !!(settings.json || settings.noReallyItsJson); // default true if unset, but allow explicit false + // These flags enable specific middleware. if ( settings.abuse ) mw.push(require('../../../middleware/abuse')(settings.abuse)); if ( settings.verified ) mw.push(require('../../../middleware/verified')); - if ( settings.json ) mw.push(express.json(_defaultJsonOptions)); - - // A hack so plain text is parsed as JSON in methods which need to be lower latency/avoid the cors roundtrip - if ( settings.noReallyItsJson ) mw.push(express.json({ ..._defaultJsonOptions, type: '*/*' })); - - mw.push(express.json({ - ..._defaultJsonOptions, - type: (req) => req.headers['content-type'] === "text/plain;actually=json", - })); + if ( shouldJson ){ + mw.push(express.json({ ..._defaultJsonOptions, type: settings.json ? undefined : settings.noReallyItsJson ? '*/*' : (req) => req.headers['content-type'] === 'text/plain;actually=json' })); + }; if ( settings.auth ) mw.push(require('../../../middleware/auth')); if ( settings.auth2 ) mw.push(require('../../../middleware/auth2')); @@ -97,8 +94,8 @@ module.exports = function eggspress (route, settings, handler) { } catch (e) { return res.status(400).send({ error: { - message: `Invalid JSON in multipart field ${key}` - } + message: `Invalid JSON in multipart field ${key}`, + }, }); } next(); @@ -179,9 +176,11 @@ module.exports = function eggspress (route, settings, handler) { }); } - if ( settings.mw ) mw.push(...settings.mw); + if ( settings.mw ){ + mw.push(...settings.mw); +} - const errorHandledHandler = async function (req, res, next) { + const errorHandledHandler = async function(req, res, next) { if ( settings.subdomain ) { if ( subdomain(req) !== settings.subdomain ) { return next(); @@ -201,7 +200,7 @@ module.exports = function eggspress (route, settings, handler) { } else await handler(req, res, next); } catch (e) { if ( config.env === 'dev' ) { - if (! (e instanceof APIError)) { + if ( ! (e instanceof APIError) ) { // Any non-APIError indicates an unhandled error (i.e. a bug) from the backend. // We add a dedicated branch to facilitate debugging. console.error(e); @@ -210,57 +209,57 @@ module.exports = function eggspress (route, settings, handler) { api_error_handler(e, req, res, next); } }; - if (settings.allowedMethods.includes('GET')) { + if ( settings.allowedMethods.includes('GET') ) { router.get(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('HEAD')) { + if ( settings.allowedMethods.includes('HEAD') ) { router.head(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('POST')) { + if ( settings.allowedMethods.includes('POST') ) { router.post(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('PUT')) { + if ( settings.allowedMethods.includes('PUT') ) { router.put(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('DELETE')) { + if ( settings.allowedMethods.includes('DELETE') ) { router.delete(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('PROPFIND')) { + if ( settings.allowedMethods.includes('PROPFIND') ) { router.propfind(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('PROPPATCH')) { + if ( settings.allowedMethods.includes('PROPPATCH') ) { router.proppatch(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('MKCOL')) { + if ( settings.allowedMethods.includes('MKCOL') ) { router.mkcol(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('COPY')) { + if ( settings.allowedMethods.includes('COPY') ) { router.copy(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('MOVE')) { + if ( settings.allowedMethods.includes('MOVE') ) { router.move(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('LOCK')) { + if ( settings.allowedMethods.includes('LOCK') ) { router.lock(route, ...mw, errorHandledHandler, ...afterMW); } - if (settings.allowedMethods.includes('UNLOCK')) { + if ( settings.allowedMethods.includes('UNLOCK') ) { router.unlock(route, ...mw, errorHandledHandler, ...afterMW); } - - if (settings.allowedMethods.includes('OPTIONS')) { + + if ( settings.allowedMethods.includes('OPTIONS') ) { router.options(route, ...mw, errorHandledHandler, ...afterMW); } return router; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/backend/src/routers/signup.js b/src/backend/src/routers/signup.js index 06955a9cc..d99bc68c8 100644 --- a/src/backend/src/routers/signup.js +++ b/src/backend/src/routers/signup.js @@ -272,10 +272,10 @@ module.exports = eggspress(['/signup'], { username, email, clean_email, password, uuid, referrer, email_confirm_code, email_confirm_token, free_storage, referred_by, audit_metadata, signup_ip, signup_ip_forwarded, - signup_user_agent, signup_origin, signup_server + signup_user_agent, signup_origin, signup_server, metadata ) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ // username req.body.username, @@ -309,6 +309,8 @@ module.exports = eggspress(['/signup'], { req.headers['origin'] ?? null, // signup_server config.server_id ?? null, + // metadata + {} ] ); diff --git a/src/backend/src/services/GetUserService.js b/src/backend/src/services/GetUserService.js index 85e16d5cd..d265f5be4 100644 --- a/src/backend/src/services/GetUserService.js +++ b/src/backend/src/services/GetUserService.js @@ -17,20 +17,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -const { Actor } = require("./auth/Actor"); -const BaseService = require("./BaseService"); -const { DB_READ } = require("./database/consts"); +const { Actor } = require('./auth/Actor'); +const BaseService = require('./BaseService'); +const { DB_READ } = require('./database/consts'); /** * Get user by one of a variety of identifying properties. - * + * * Pass `cached: false` to options to force a database read. * Pass `force: true` to options to force a primary database read. - * + * * This provides the functionality of `get_user` (helpers.js) * as a service so that other services can register identifying * properties for caching. - * + * * The original `get_user` function now uses this service. */ class GetUserService extends BaseService { @@ -38,7 +38,7 @@ class GetUserService extends BaseService { * Constructor for GetUserService. * Initializes the set of identifying properties used to retrieve user data. */ - _construct () { + _construct() { this.id_properties = new Set(); this.id_properties.add('username'); @@ -52,35 +52,35 @@ class GetUserService extends BaseService { * Initializes the GetUserService instance. * This method prepares any necessary internal structures or states. * It is called automatically upon instantiation of the service. - * + * * @returns {Promise} A promise that resolves when the initialization is complete. */ - async _init () { + async _init() { } /** * Retrieves a user object based on the provided options. - * + * * This method queries the user from cache or database, - * depending on the caching options provided. If the user - * is found, it also calls the 'whoami' service to enrich + * depending on the caching options provided. If the user + * is found, it also calls the 'whoami' service to enrich * the user details before returning. - * + * * @param {Object} options - The options for retrieving the user. * @param {boolean} [options.cached=true] - Indicates if caching should be used. * @param {boolean} [options.force=false] - Forces a read from the database regardless of cache. * @returns {Promise} The user object if found, else null. */ - async get_user (options) { + async get_user(options) { const user = await this.get_user_(options); if ( ! user ) return null; - + const svc_whoami = this.services.get('whoami'); await svc_whoami.get_details({ user }, user); return user; } - - async refresh_actor (actor) { + + async refresh_actor(actor) { if ( actor.type.user ) { actor.type.user = await this.get_user({ username: actor.type.user.username, @@ -90,7 +90,7 @@ class GetUserService extends BaseService { return actor; } - async get_user_ (options) { + async get_user_(options) { const services = this.services; /** @type BaseDatabaseAccessService */ @@ -135,13 +135,19 @@ class GetUserService extends BaseService { kv.set(`users:${prop}:${user[prop]}`, user); } } - } catch (e) { + } catch ( e ) { console.error(e); } + if ( user.metadata && typeof user.metadata === 'string' ){ + user.metadata = JSON.parse(user.metadata); + } else if ( !user.metadata ){ + user.metadata = {}; + } return user; } - register_id_property (prop) { + + register_id_property(prop) { this.id_properties.add(prop); } } diff --git a/src/backend/src/services/MeteringService/subPolicies/index.ts b/src/backend/src/services/MeteringService/subPolicies/index.ts index 44a6f0b5c..4d1ee7178 100644 --- a/src/backend/src/services/MeteringService/subPolicies/index.ts +++ b/src/backend/src/services/MeteringService/subPolicies/index.ts @@ -1,5 +1,5 @@ -import { REGISTERED_USER_FREE } from "./registeredUserFreePolicy"; -import { TEMP_USER_FREE } from "./tempUserFreePolicy"; +import { REGISTERED_USER_FREE } from './registeredUserFreePolicy'; +import { TEMP_USER_FREE } from './tempUserFreePolicy'; export const SUB_POLICIES = [ TEMP_USER_FREE, diff --git a/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts b/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts index 1eae5e602..6c2a53697 100644 --- a/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts +++ b/src/backend/src/services/MeteringService/subPolicies/registeredUserFreePolicy.ts @@ -4,4 +4,4 @@ export const REGISTERED_USER_FREE = { id: 'user_free', monthUsageAllowance: toMicroCents(0.50), monthlyStorageAllowance: 100 * 1024 * 1024, // 100MiB -}; \ No newline at end of file +} as const; \ No newline at end of file diff --git a/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts b/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts index 47eb9e0bf..73f80da45 100644 --- a/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts +++ b/src/backend/src/services/MeteringService/subPolicies/tempUserFreePolicy.ts @@ -4,4 +4,4 @@ export const TEMP_USER_FREE = { id: 'temp_free', monthUsageAllowance: toMicroCents(0.25), monthlyStorageAllowance: 100 * 1024 * 1024, // 100MiB -}; \ No newline at end of file +} as const; \ No newline at end of file diff --git a/src/backend/src/services/User.d.ts b/src/backend/src/services/User.d.ts new file mode 100644 index 000000000..6d0a92487 --- /dev/null +++ b/src/backend/src/services/User.d.ts @@ -0,0 +1,8 @@ +import { SUB_POLICIES } from './MeteringService/subPolicies'; + +export interface IUser { uuid: string, + username: string, + email: string, + subscription?: (typeof SUB_POLICIES)[number]['id'], + metadata?: Record & { hasDevAccountAccess?: boolean } +} \ No newline at end of file diff --git a/src/backend/src/services/UserService.d.ts b/src/backend/src/services/UserService.d.ts new file mode 100644 index 000000000..1423543db --- /dev/null +++ b/src/backend/src/services/UserService.d.ts @@ -0,0 +1,12 @@ +import type { BaseService } from './BaseService'; +import type { IUser } from './User'; + +export interface IInsertResult { + insertId: number; +} + +export class UserService extends BaseService { + get_system_dir(): unknown; + generate_default_fsentries(args: { user: IUser }): Promise; + updateUserMetadata(user: IUser, updatedMetadata: Record): Promise; +} diff --git a/src/backend/src/services/UserService.js b/src/backend/src/services/UserService.js index a0a495570..adf2b25ce 100644 --- a/src/backend/src/services/UserService.js +++ b/src/backend/src/services/UserService.js @@ -1,51 +1,47 @@ /* * 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 { RootNodeSelector, NodeChildSelector } = require("../filesystem/node/selectors"); -const { invalidate_cached_user } = require("../helpers"); -const BaseService = require("./BaseService"); -const { DB_WRITE } = require("./database/consts"); +const { RootNodeSelector, NodeChildSelector } = require('../filesystem/node/selectors'); +const { invalidate_cached_user } = require('../helpers'); +const BaseService = require('./BaseService'); +const { DB_WRITE } = require('./database/consts'); class UserService extends BaseService { static MODULES = { uuidv4: require('uuid').v4, }; - async _init () { + async _init() { this.db = this.services.get('database').get(DB_WRITE, 'user-service'); this.dir_system = null; } - async ['__on_filesystem.ready'] () { + async ['__on_filesystem.ready']() { const svc_fs = this.services.get('filesystem'); // Ensure system user has a home directory - const dir_system = await svc_fs.node( - new NodeChildSelector( - new RootNodeSelector(), - 'system' - ) - ); + const dir_system = await svc_fs.node(new NodeChildSelector(new RootNodeSelector(), + 'system')); if ( ! await dir_system.exists() ) { const svc_getUser = this.services.get('get-user'); await this.generate_default_fsentries({ - user: await svc_getUser.get_user({ username: 'system' }) + user: await svc_getUser.get_user({ username: 'system' }), }); } @@ -54,15 +50,15 @@ class UserService extends BaseService { this.services.emit('user.system-user-ready'); } - get_system_dir () { + get_system_dir() { return this.dir_system; } // used to be called: generate_system_fsentries - async generate_default_fsentries ({ user }) { - + async generate_default_fsentries({ user }) { + this.log.noticeme('YES THIS WAS USED'); - + // Note: The comment below is outdated as we now do parallel writes for // all filesystem operations. However, there may still be some // performance hit so this requires further investigation. @@ -74,7 +70,7 @@ class UserService extends BaseService { // by combining as many queries as we can into one and avoiding multiple back-and-forth // with the DB server, we can speed this process up significantly. - const ts = Date.now()/1000; + const ts = Date.now() / 1000; // Generate UUIDs for all the default folders and files const uuidv4 = this.modules.uuidv4; @@ -88,8 +84,7 @@ class UserService extends BaseService { let videos_uuid = uuidv4(); let public_uuid = uuidv4(); - const insert_res = await this.db.write( - `INSERT INTO fsentries + const insert_res = await this.db.write(`INSERT INTO fsentries (uuid, parent_uid, user_id, name, path, is_dir, created, modified, immutable) VALUES ( ?, ?, ?, ?, ?, true, ?, ?, true), ( ?, ?, ?, ?, ?, true, ?, ?, true), @@ -100,25 +95,24 @@ class UserService extends BaseService { ( ?, ?, ?, ?, ?, true, ?, ?, true), ( ?, ?, ?, ?, ?, true, ?, ?, true) `, - [ - // Home - home_uuid, null, user.id, user.username, `/${user.username}`, ts, ts, - // Trash - trash_uuid, home_uuid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts, - // AppData - appdata_uuid, home_uuid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts, - // Desktop - desktop_uuid, home_uuid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts, - // Documents - documents_uuid, home_uuid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts, - // Pictures - pictures_uuid, home_uuid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts, - // Videos - videos_uuid, home_uuid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts, - // Public - public_uuid, home_uuid, user.id, 'Public', `/${user.username}/Public`, ts, ts, - ] - ); + [ + // Home + home_uuid, null, user.id, user.username, `/${user.username}`, ts, ts, + // Trash + trash_uuid, home_uuid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts, + // AppData + appdata_uuid, home_uuid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts, + // Desktop + desktop_uuid, home_uuid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts, + // Documents + documents_uuid, home_uuid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts, + // Pictures + pictures_uuid, home_uuid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts, + // Videos + videos_uuid, home_uuid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts, + // Public + public_uuid, home_uuid, user.id, 'Public', `/${user.username}/Public`, ts, ts, + ]); // https://stackoverflow.com/a/50103616 let trash_id = insert_res.insertId; @@ -135,20 +129,29 @@ class UserService extends BaseService { // TODO: pass to IIAFE manager to avoid unhandled promise rejection // (IIAFE manager doesn't exist yet, hence this is a TODO) - this.db.write( - `UPDATE user SET + this.db.write(`UPDATE user SET trash_uuid=?, appdata_uuid=?, desktop_uuid=?, documents_uuid=?, pictures_uuid=?, videos_uuid=?, public_uuid=?, trash_id=?, appdata_id=?, desktop_id=?, documents_id=?, pictures_id=?, videos_id=?, public_id=? - WHERE id=?`, - [ - trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid, public_uuid, - trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id, public_id, - user.id - ] - ); + [ + trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid, public_uuid, + trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id, public_id, + user.id, + ]); invalidate_cached_user(user); } + + async updateUserMetadata(user, updatedMetadata){ + + let metadata = user.metadata; + if ( !Object.keys(metadata).length ){ + metadata = updatedMetadata; + } else { + metadata = { ...metadata, ...updatedMetadata }; + } + + await this.db.write('UPDATE user SET metadata=? WHERE id=?', [metadata]); + } } module.exports = { diff --git a/src/backend/src/services/auth/Actor.d.ts b/src/backend/src/services/auth/Actor.d.ts index fa4730f33..3e8f1c191 100644 --- a/src/backend/src/services/auth/Actor.d.ts +++ b/src/backend/src/services/auth/Actor.d.ts @@ -1,15 +1,17 @@ +import { IUser } from '../User'; + export class SystemActorType { get uid(): string; - get_related_type(type_class: any): SystemActorType; + get_related_type(type_class: unknown): SystemActorType; } export class Actor { type: { app: { uid: string } - user: { uuid: string, username: string, email: string, subscription?: (typeof SUB_POLICIES)[keyof typeof SUB_POLICIES]['id'] } - } + user: IUser + }; get uid(): string; clone(): Actor; static get_system_actor(): Actor; - static adapt(actor?: any): Actor; + static adapt(actor?: Actor): Actor; } \ No newline at end of file diff --git a/src/backend/src/services/database/BaseDatabaseAccessService.d.ts b/src/backend/src/services/database/BaseDatabaseAccessService.d.ts new file mode 100644 index 000000000..c8b238d31 --- /dev/null +++ b/src/backend/src/services/database/BaseDatabaseAccessService.d.ts @@ -0,0 +1,29 @@ +import { BaseService } from "../BaseService"; + +export type DBMode = "DB_WRITE" | "DB_READ"; + +export interface IBaseDatabaseAccessService { + get(): this; + read(query: string, params?: any[]): Promise; + tryHardRead(query: string, params?: any[]): Promise; + requireRead(query: string, params?: any[]): Promise; + pread(query: string, params?: any[]): Promise; + write(query: string, params?: any[]): Promise; + insert(table_name: string, data: Record): Promise; + batch_write(statements: string[]): any; +} + +export class BaseDatabaseAccessService extends BaseService implements IBaseDatabaseAccessService { + static DB_WRITE: DBMode; + static DB_READ: DBMode; + case(choices: Record): T; + get(): this; + read(query: string, params?: any[]): Promise; + tryHardRead(query: string, params?: any[]): Promise; + requireRead(query: string, params?: any[]): Promise; + pread(query: string, params?: any[]): Promise; + write(query: string, params?: any[]): Promise; + insert(table_name: string, data: Record): Promise; + batch_write(statements: string[]): any; + _gen_insert_sql(table_name: string, data: Record): string; +} diff --git a/src/backend/src/services/database/sqlite_setup/0040_add_user_metadata.sql b/src/backend/src/services/database/sqlite_setup/0040_add_user_metadata.sql new file mode 100644 index 000000000..698f262af --- /dev/null +++ b/src/backend/src/services/database/sqlite_setup/0040_add_user_metadata.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD COLUMN `metadata` TEXT DEFAULT '{}'; \ No newline at end of file diff --git a/src/backend/src/util/expressutil.js b/src/backend/src/util/expressutil.js index a0ee952a7..84fee8e34 100644 --- a/src/backend/src/util/expressutil.js +++ b/src/backend/src/util/expressutil.js @@ -16,34 +16,33 @@ * 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 eggspress = require('../api/eggspress'); -const Endpoint = function Endpoint (spec, handler) { +const Endpoint = function Endpoint(spec, handler) { return { - attach (route) { + attach(route) { const eggspress_options = { allowedMethods: spec.methods ?? ['GET'], ...(spec.subdomain ? { subdomain: spec.subdomain } : {}), ...(spec.parameters ? { parameters: spec.parameters } : {}), ...(spec.alias ? { alias: spec.alias } : {}), ...(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) { + but(newSpec) { // TODO: add merge with '$' behaviors (like config has) return Endpoint({ ...spec, ...newSpec, }); - } + }, }; -} +}; module.exports = { Endpoint, diff --git a/src/dev-center/index.html b/src/dev-center/index.html index 7a371e52c..ea394a8c1 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -31,45 +31,53 @@