diff --git a/doc/contributors/extensions/README.md b/doc/contributors/extensions/README.md index 0af053c18..be00793fc 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, mw: [] }, (req, res) => { +extension.get('/user-count', { noauth: true }, (req, res) => { const [count] = await ext.db.read( 'SELECT COUNT(*) as c FROM `user`' ); diff --git a/eslint.config.js b/eslint.config.js index 3fc8b1afc..26cff7ee0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,7 +45,6 @@ const rules = { 'no-undef': 'error', 'custom/control-structure-spacing': 'error', '@stylistic/no-trailing-spaces': 'error', - '@stylistic/space-before-blocks': ['error', 'always'], }; export default defineConfig([ @@ -141,7 +140,48 @@ export default defineConfig([ i18n: 'readonly', }, }, - rules, + rules: { + + 'no-unused-vars': ['error', { + 'vars': 'all', + 'args': 'after-used', + 'caughtErrors': 'all', + 'ignoreRestSiblings': false, + 'ignoreUsingDeclarations': false, + 'reportUsedIgnorePattern': false, + 'argsIgnorePattern': '^_', + 'caughtErrorsIgnorePattern': '^_', + 'destructuredArrayIgnorePattern': '^_', + }], + '@stylistic/curly-newline': ['error', 'always'], + '@stylistic/object-curly-spacing': ['error', 'always'], + '@stylistic/indent': ['error', 4, { + 'CallExpression': { arguments: 4 }, + }], + '@stylistic/indent-binary-ops': ['error', 4], + '@stylistic/array-bracket-newline': ['error', 'consistent'], + '@stylistic/semi': ['error', 'always'], + '@stylistic/quotes': ['error', 'single', { 'avoidEscape': true }], + '@stylistic/function-call-argument-newline': ['error', 'consistent'], + '@stylistic/arrow-spacing': ['error', { before: true, after: true }], + '@stylistic/space-before-function-paren': ['error', { 'anonymous': 'never', 'named': 'never', 'asyncArrow': 'always', 'catch': 'never' }], + '@stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], + '@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }], + '@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }], + '@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }], + '@stylistic/comma-dangle': ['error', 'always-multiline'], + '@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }], + '@stylistic/dot-location': ['error', 'property'], + '@stylistic/space-infix-ops': ['error'], + 'no-template-curly-in-string': 'error', + 'prefer-template': 'error', + 'no-undef': 'error', + 'no-useless-concat': 'error', + 'template-curly-spacing': ['error', 'never'], + curly: ['error', 'multi-line'], + 'custom/control-structure-spacing': 'error', + '@stylistic/no-trailing-spaces': 'error', + }, }, { files: ['**/*.{js,mjs,cjs,ts}'], @@ -153,7 +193,47 @@ export default defineConfig([ i18n: 'readonly', }, }, - rules, + rules: { + 'no-unused-vars': ['error', { + 'vars': 'all', + 'args': 'after-used', + 'caughtErrors': 'all', + 'ignoreRestSiblings': false, + 'ignoreUsingDeclarations': false, + 'reportUsedIgnorePattern': false, + 'argsIgnorePattern': '^_', + 'caughtErrorsIgnorePattern': '^_', + 'destructuredArrayIgnorePattern': '^_', + }], + '@stylistic/curly-newline': ['error', 'always'], + '@stylistic/object-curly-spacing': ['error', 'always'], + '@stylistic/indent': ['error', 4, { + 'CallExpression': { arguments: 4 }, + }], + '@stylistic/indent-binary-ops': ['error', 4], + '@stylistic/array-bracket-newline': ['error', 'consistent'], + '@stylistic/semi': ['error', 'always'], + '@stylistic/quotes': ['error', 'single', { 'avoidEscape': true }], + '@stylistic/function-call-argument-newline': ['error', 'consistent'], + '@stylistic/arrow-spacing': ['error', { before: true, after: true }], + '@stylistic/space-before-function-paren': ['error', { 'anonymous': 'never', 'named': 'never', 'asyncArrow': 'always', 'catch': 'never' }], + '@stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], + '@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }], + '@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }], + '@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }], + '@stylistic/comma-dangle': ['error', 'always-multiline'], + '@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }], + '@stylistic/dot-location': ['error', 'property'], + '@stylistic/space-infix-ops': ['error'], + 'no-template-curly-in-string': 'error', + 'prefer-template': 'error', + 'no-undef': 'error', + 'no-useless-concat': 'error', + 'template-curly-spacing': ['error', 'never'], + curly: ['error', 'multi-line'], + 'custom/control-structure-spacing': 'error', + '@stylistic/no-trailing-spaces': 'error', + }, extends: ['js/recommended'], plugins: { js, diff --git a/extensions/ExtensionController/.gitignore b/extensions/ExtensionController/.gitignore deleted file mode 100644 index 69a24fb74..000000000 --- a/extensions/ExtensionController/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.js -*.map \ No newline at end of file diff --git a/extensions/ExtensionController/package.json b/extensions/ExtensionController/package.json deleted file mode 100644 index fda136aaf..000000000 --- a/extensions/ExtensionController/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 deleted file mode 100644 index 6a7aab6de..000000000 --- a/extensions/ExtensionController/src/ExtensionController.ts +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 479485377..000000000 --- a/extensions/ExtensionController/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -//@puter priority -1000 -import * as extensionControllerExports from './ExtensionController.js'; - -extension.exports = { ...extensionControllerExports }; diff --git a/extensions/ExtensionController/tsconfig.json b/extensions/ExtensionController/tsconfig.json deleted file mode 100644 index f1241ce1b..000000000 --- a/extensions/ExtensionController/tsconfig.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - // 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 830bb1659..1ad3fca4d 100644 --- a/extensions/api.d.ts +++ b/extensions/api.d.ts @@ -1,23 +1,17 @@ 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, - /** @deprecated use actor instead */ - user: IUser + actor: Actor } } } @@ -26,11 +20,6 @@ interface EndpointOptions { allowedMethods?: string[] subdomain?: string noauth?: boolean - mw?: RequestHandler[] - otherOpts?: Record & { - json?: boolean - noReallyItsJson?: boolean - } } type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; @@ -61,15 +50,10 @@ 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, - on(event: string, listener: (...args: T)=> void): void, // TODO DS: type events better 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 fe63cd332..1697c378e 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,7 +94,6 @@ 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 } : {}), }; @@ -149,35 +148,37 @@ 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; @@ -188,7 +189,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 = {}; } @@ -199,7 +200,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) } } } @@ -220,6 +221,5 @@ 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 cb233e2ad..fad45b361 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,53 +114,59 @@ 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) ) { @@ -172,28 +178,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_(); @@ -215,7 +221,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_(); @@ -230,52 +236,8 @@ class Extension extends AdvancedBase { methods: ['POST'], }); } - - /** - * 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) { + + use (...args) { this.ensure_service_(); this.service.expressThings_.push({ type: 'router', @@ -284,7 +246,7 @@ class Extension extends AdvancedBase { } get preinit() { - return (function(callback) { + return (function (callback) { this.on('preinit', callback); }).bind(this); } @@ -295,8 +257,7 @@ class Extension extends AdvancedBase { }); } if ( callback === null ) { - this.only_one_preinit_fn = () => { - }; + this.only_one_preinit_fn = () => {}; } this.only_one_preinit_fn = callback; } @@ -306,20 +267,19 @@ 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; @@ -357,10 +317,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; } @@ -373,4 +333,4 @@ class Extension extends AdvancedBase { module.exports = { Extension, -}; +} diff --git a/src/backend/src/ExtensionService.js b/src/backend/src/ExtensionService.js index d08927158..8a3000661 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,9 +64,8 @@ class ExtensionServiceState extends AdvancedBase { route: path, handler: handler, ...(options.subdomain ? { subdomain: options.subdomain } : {}), - otherOpts: options.otherOpts || {}, }); - + this.expressThings_.push({ type: 'endpoint', value: endpoint }); } } @@ -77,15 +76,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'); @@ -114,20 +113,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 @@ -153,7 +152,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', {}, { @@ -161,7 +160,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', {}, { @@ -169,7 +168,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', {}, { @@ -178,7 +177,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/WebServerService.js b/src/backend/src/modules/web/WebServerService.js index c87c4bbd3..bd7ea879a 100644 --- a/src/backend/src/modules/web/WebServerService.js +++ b/src/backend/src/modules/web/WebServerService.js @@ -626,7 +626,7 @@ class WebServerService extends BaseService { const allowed_headers = [ "Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization", "sentry-trace", "baggage", - "Depth", "Destination", "Overwrite", "If", "Lock-Token", "DAV", "stripe-signature", + "Depth", "Destination", "Overwrite", "If", "Lock-Token", "DAV" ]; // Request headers to allow diff --git a/src/backend/src/modules/web/lib/eggspress.js b/src/backend/src/modules/web/lib/eggspress.js index b882c2171..49ae0571e 100644 --- a/src/backend/src/modules/web/lib/eggspress.js +++ b/src/backend/src/modules/web/lib/eggspress.js @@ -43,25 +43,28 @@ 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 ( shouldJson ){ - mw.push(express.json({ ..._defaultJsonOptions, type: settings.json ? undefined : settings.noReallyItsJson ? '*/*' : (req) => req.headers['content-type'] === 'text/plain;actually=json' })); - }; + 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 ( settings.auth ) mw.push(require('../../../middleware/auth')); if ( settings.auth2 ) mw.push(require('../../../middleware/auth2')); @@ -94,8 +97,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(); @@ -176,11 +179,9 @@ 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(); @@ -200,7 +201,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); @@ -209,57 +210,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 38f1770ef..06955a9cc 100644 --- a/src/backend/src/routers/signup.js +++ b/src/backend/src/routers/signup.js @@ -16,22 +16,22 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -'use strict'; -const { get_taskbar_items, send_email_verification_code, send_email_verification_token, username_exists, invalidate_cached_user_by_id, get_user } = require('../helpers'); +"use strict" +const {get_taskbar_items, send_email_verification_code, send_email_verification_token, username_exists, invalidate_cached_user_by_id, get_user } = require('../helpers'); const config = require('../config'); const eggspress = require('../api/eggspress'); const { Context } = require('../util/context'); const { DB_WRITE } = require('../services/database/consts'); const { generate_identifier } = require('../util/identifier'); -const { is_temp_users_disabled: lazy_temp_users, - is_user_signup_disabled: lazy_user_signup } = require('../helpers'); +const { is_temp_users_disabled: lazy_temp_users, + is_user_signup_disabled: lazy_user_signup } = require("../helpers") const { requireCaptcha } = require('../modules/captcha/middleware/captcha-middleware'); -async function generate_random_username() { +async function generate_random_username () { let username; do { username = generate_identifier(); - } while ( await username_exists(username) ); + } while (await username_exists(username)); return username; } @@ -46,16 +46,14 @@ module.exports = eggspress(['/signup'], { no_bots: true, // puter_origin: false, shadow_ban_responder: (req, res) => { - res.status(400).send('email username mismatch; please provide a password'); - }, + res.status(400).send(`email username mismatch; please provide a password`); + } }, mw: [requireCaptcha({ strictMode: true, eventType: 'signup' })], // Conditionally require captcha for signup }, async (req, res, next) => { // either api. subdomain or no subdomain - if ( require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '' ) - { + if(require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '') next(); - } const svc_edgeRateLimit = req.services.get('edge-rate-limit'); if ( ! svc_edgeRateLimit.check('signup') ) { @@ -64,32 +62,31 @@ module.exports = eggspress(['/signup'], { // modules const db = req.services.get('database').get(DB_WRITE, 'auth'); - const bcrypt = require('bcrypt'); + const bcrypt = require('bcrypt') const { v4: uuidv4 } = require('uuid'); - const jwt = require('jsonwebtoken'); - const validator = require('validator'); + const jwt = require('jsonwebtoken') + const validator = require('validator') let uuid_user; const svc_auth = Context.get('services').get('auth'); const svc_authAudit = Context.get('services').get('auth-audit'); svc_authAudit.record({ requester: Context.get('requester'), - action: req.body.is_temp ? 'signup:temp' : 'signup:real', + action: req.body.is_temp ? `signup:temp` : `signup:real`, body: req.body, }); // check bot trap, if `p102xyzname` is anything but an empty string it means // that a bot has filled the form // doesn't apply to temp users - if ( !req.body.is_temp && req.body.p102xyzname !== '' ) - { + if(!req.body.is_temp && req.body.p102xyzname !== '') return res.send(); - } + // cloudflare turnstile validation // // ref: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ - if ( config.services?.['cloudflare-turnstile']?.enabled ) { + if (config.services?.['cloudflare-turnstile']?.enabled) { const formData = new FormData(); formData.append('secret', config.services?.['cloudflare-turnstile']?.secret_key); formData.append('response', req.body['cf-turnstile-response']); @@ -97,14 +94,12 @@ module.exports = eggspress(['/signup'], { const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', - body: formData, + body: formData }); const result = await response.json(); - if ( !result.success ) - { + if (!result.success) return res.status(400).send('captcha verification failed'); - } } // send event @@ -117,7 +112,7 @@ module.exports = eggspress(['/signup'], { }; const svc_event = Context.get('services').get('event'); - await svc_event.emit('puter.signup', event); + await svc_event.emit('puter.signup', event) if ( ! event.allow ) { return res.status(400).send(event.error ?? 'You are not allowed to sign up.'); @@ -125,7 +120,9 @@ module.exports = eggspress(['/signup'], { // check if user is already logged in if ( req.body.is_temp && req.cookies[config.cookie_name] ) { - const { user, token } = await svc_auth.check_session(req.cookies[config.cookie_name]); + const { user, token } = await svc_auth.check_session( + req.cookies[config.cookie_name] + ); res.cookie(config.cookie_name, token, { sameSite: 'none', secure: true, @@ -144,7 +141,7 @@ module.exports = eggspress(['/signup'], { requires_email_confirmation: user.requires_email_confirmation, is_temp: (user.password === null && user.email === null), taskbar_items: await get_taskbar_items(user), - }, + } }); } } @@ -152,19 +149,19 @@ module.exports = eggspress(['/signup'], { const is_temp_users_disabled = await lazy_temp_users(); const is_user_signup_disabled = await lazy_user_signup(); - if ( is_temp_users_disabled && is_user_signup_disabled ) { + if (is_temp_users_disabled && is_user_signup_disabled) { return res.status(403).send('User signup and Temporary users are disabled.'); } - if ( !req.body.is_temp && is_user_signup_disabled ) { + if (!req.body.is_temp && is_user_signup_disabled) { return res.status(403).send('User signup is disabled.'); - } + } - if ( req.body.is_temp && is_temp_users_disabled ) { + if (req.body.is_temp && is_temp_users_disabled) { return res.status(403).send('Temporary users are disabled.'); } - if ( req.body.is_temp && event.no_temp_user ) { + if (req.body.is_temp && event.no_temp_user) { return res.status(403).send('You must login or signup.'); } @@ -172,101 +169,74 @@ module.exports = eggspress(['/signup'], { req.body.username = req.body.username ?? await generate_random_username(); req.body.email = req.body.email ?? req.body.username + '@gmail.com'; req.body.password = req.body.password ?? 'sadasdfasdfsadfsa'; - + // send_confirmation_code req.body.send_confirmation_code = req.body.send_confirmation_code ?? true; // username is required - if ( !req.body.username ) - { - return res.status(400).send('Username is required'); - } + if(!req.body.username) + return res.status(400).send('Username is required') // username must be a string - else if ( typeof req.body.username !== 'string' ) - { - return res.status(400).send('username must be a string.'); - } + else if (typeof req.body.username !== 'string') + return res.status(400).send('username must be a string.') // check if username is valid - else if ( !req.body.username.match(config.username_regex) ) - { - return res.status(400).send('Username can only contain letters, numbers and underscore (_).'); - } + else if(!req.body.username.match(config.username_regex)) + return res.status(400).send('Username can only contain letters, numbers and underscore (_).') // check if username is of proper length - else if ( req.body.username.length > config.username_max_length ) - { - return res.status(400).send(`Username cannot be longer than ${config.username_max_length} characters.`); - } + else if(req.body.username.length > config.username_max_length) + return res.status(400).send(`Username cannot be longer than ${config.username_max_length} characters.`) // check if username matches any reserved words - else if ( config.reserved_words.includes(req.body.username) ) - { - return res.status(400).send({ message: 'This username is not available.' }); - } + else if(config.reserved_words.includes(req.body.username)) + return res.status(400).send({message: 'This username is not available.'}); // TODO: DRY: change_email.js - else if ( !req.body.is_temp && !req.body.email ) - { + else if(!req.body.is_temp && !req.body.email) return res.status(400).send('Email is required'); - } // email, if present, must be a string - else if ( req.body.email && typeof req.body.email !== 'string' ) - { - return res.status(400).send('email must be a string.'); - } + else if (req.body.email && typeof req.body.email !== 'string') + return res.status(400).send('email must be a string.') // if email is present, validate it - else if ( !req.body.is_temp && !validator.isEmail(req.body.email) ) - { - return res.status(400).send('Please enter a valid email address.'); - } - else if ( !req.body.is_temp && !req.body.password ) - { + else if(!req.body.is_temp && !validator.isEmail(req.body.email)) + return res.status(400).send('Please enter a valid email address.') + else if(!req.body.is_temp && !req.body.password) return res.status(400).send('Password is required'); - } // password, if present, must be a string - else if ( req.body.password && typeof req.body.password !== 'string' ) - { - return res.status(400).send('password must be a string.'); - } - else if ( !req.body.is_temp && req.body.password.length < config.min_pass_length ) - { + else if (req.body.password && typeof req.body.password !== 'string') + return res.status(400).send('password must be a string.') + else if(!req.body.is_temp && req.body.password.length < config.min_pass_length) return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`); - } const svc_cleanEmail = req.services.get('clean-email'); const clean_email = svc_cleanEmail.clean(req.body.email); - - if ( !req.body.is_temp && ! await svc_cleanEmail.validate(clean_email) ) { + + if (!req.body.is_temp && ! await svc_cleanEmail.validate(clean_email) ) { return res.status(400).send('This email does not seem to be valid.'); } // duplicate username check - if ( await username_exists(req.body.username) ) - { + if(await username_exists(req.body.username)) return res.status(400).send('This username already exists in our database. Please use another one.'); - } // Email check is here :: Add condition for email_confirmed=1 // duplicate email check (pseudo-users don't count) - let rows2 = await db.read(`SELECT EXISTS( + let rows2 = await db.read( + `SELECT EXISTS( SELECT 1 FROM user WHERE (email=? OR clean_email=?) AND email_confirmed=1 AND password IS NOT NULL ) AS email_exists`, [req.body.email, clean_email]); - if ( rows2[0].email_exists ) - { + if(rows2[0].email_exists) return res.status(400).send('This email already exists in our database. Please use another one.'); - } // get pseudo user, if exists - let pseudo_user = await db.read('SELECT * FROM user WHERE email = ? AND password IS NULL', [req.body.email]); + let pseudo_user = await db.read(`SELECT * FROM user WHERE email = ? AND password IS NULL`, [req.body.email]); pseudo_user = pseudo_user[0]; // get uuid user, if exists - if ( req.body.uuid ) { - uuid_user = await db.read('SELECT * FROM user WHERE uuid = ? LIMIT 1', [req.body.uuid]); + if(req.body.uuid){ + uuid_user = await db.read(`SELECT * FROM user WHERE uuid = ? LIMIT 1`, [req.body.uuid]); uuid_user = uuid_user[0]; } // email confirmation is required by default unless: // Pseudo user converting and matching uuid is provided let email_confirmation_required = 1; - if ( pseudo_user && uuid_user && pseudo_user.id === uuid_user.id ) - { + if(pseudo_user && uuid_user && pseudo_user.id === uuid_user.id) email_confirmation_required = 0; - } // ----------------------------------- // Get referral user @@ -295,116 +265,95 @@ module.exports = eggspress(['/signup'], { server: config.server_id, }; - if ( pseudo_user === undefined ) { - insert_res = await db.write(`INSERT INTO user + if(pseudo_user === undefined){ + insert_res = await db.write( + `INSERT INTO user ( - 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 + 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 ) VALUES - (?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?)`, - [ - // username - req.body.username, - // email - req.body.is_temp ? null : req.body.email, - // normalized email - req.body.is_temp ? null : clean_email, - // password - req.body.is_temp ? null : await bcrypt.hash(req.body.password, 8), - // uuid - user_uuid, - // referrer - req.body.referrer ?? null, - // email_confirm_code - '' + email_confirm_code, - // email_confirm_token - email_confirm_token, - // free_storage - config.storage_capacity, - // referred_by - referred_by_user ? referred_by_user.id : null, - // audit_metadata - JSON.stringify(audit_metadata), - // signup_ip - req.connection.remoteAddress ?? null, - // signup_ip_fwd - req.headers['x-forwarded-for'] ?? null, - // signup_user_agent - req.headers['user-agent'] ?? null, - // signup_origin - req.headers['origin'] ?? null, - // signup_server - config.server_id ?? null, - ]); + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + // username + req.body.username, + // email + req.body.is_temp ? null : req.body.email, + // normalized email + req.body.is_temp ? null : clean_email, + // password + req.body.is_temp ? null : await bcrypt.hash(req.body.password, 8), + // uuid + user_uuid, + // referrer + req.body.referrer ?? null, + // email_confirm_code + '' + email_confirm_code, + // email_confirm_token + email_confirm_token, + // free_storage + config.storage_capacity, + // referred_by + referred_by_user ? referred_by_user.id : null, + // audit_metadata + JSON.stringify(audit_metadata), + // signup_ip + req.connection.remoteAddress ?? null, + // signup_ip_fwd + req.headers['x-forwarded-for'] ?? null, + // signup_user_agent + req.headers['user-agent'] ?? null, + // signup_origin + req.headers['origin'] ?? null, + // signup_server + config.server_id ?? null, + ] + ); // record activity - db.write('UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1', - [insert_res.insertId]); - + db.write( + 'UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1', + [insert_res.insertId] + ); + // TODO: cache group id const svc_group = req.services.get('group'); await svc_group.add_users({ uid: req.body.is_temp ? config.default_temp_group : config.default_user_group, - users: [req.body.username], + users: [req.body.username] }); } // ----------------------------------- // Pseudo User converting // ----------------------------------- - else { - insert_res = await db.write(`UPDATE user SET + else{ + insert_res = await db.write( + `UPDATE user SET username = ?, password = ?, uuid = ?, email_confirm_code = ?, email_confirm_token = ?, email_confirmed = ?, requires_email_confirmation = 1, referred_by = ? WHERE id = ?`, - [ - // username - req.body.username, - // password - await bcrypt.hash(req.body.password, 8), - // uuid - user_uuid, - // email_confirm_code - '' + email_confirm_code, - // email_confirm_token - email_confirm_token, - // email_confirmed - !email_confirmation_required, - // id - pseudo_user.id, - // referred_by - referred_by_user ? referred_by_user.id : null, - ]); + [ + // username + req.body.username, + // password + await bcrypt.hash(req.body.password, 8), + // uuid + user_uuid, + // email_confirm_code + '' + email_confirm_code, + // email_confirm_token + email_confirm_token, + // email_confirmed + !email_confirmation_required, + // id + pseudo_user.id, + // referred_by + referred_by_user ? referred_by_user.id : null, + ] + ); // TODO: cache group ids const svc_group = req.services.get('group'); @@ -414,7 +363,7 @@ module.exports = eggspress(['/signup'], { }); await svc_group.add_users({ uid: config.default_user_group, - users: [req.body.username], + users: [req.body.username] }); // record activity @@ -426,8 +375,10 @@ module.exports = eggspress(['/signup'], { // todo if pseudo user, assign directly no need to do another DB lookup const user_id = (pseudo_user === undefined) ? insert_res.insertId : pseudo_user.id; - const [user] = await db.pread('SELECT * FROM `user` WHERE `id` = ? LIMIT 1', - [user_id]); + const [user] = await db.pread( + 'SELECT * FROM `user` WHERE `id` = ? LIMIT 1', + [user_id] + ); // create token for login const { token } = await svc_auth.create_session_token(user, { @@ -439,15 +390,11 @@ module.exports = eggspress(['/signup'], { // email confirmation //------------------------------------------------------------- // Email confirmation from signup is sent here - if ( (!req.body.is_temp && email_confirmation_required) || user.requires_email_confirmation ) { - if ( req.body.send_confirmation_code || user.requires_email_confirmation ) - { + if((!req.body.is_temp && email_confirmation_required) || user.requires_email_confirmation){ + if(req.body.send_confirmation_code || user.requires_email_confirmation) send_email_verification_code(email_confirm_code, user.email); - } else - { send_email_verification_token(user.email_confirm_token, user.email, user.uuid); - } } //------------------------------------------------------------- @@ -473,7 +420,7 @@ module.exports = eggspress(['/signup'], { }); // add to mailchimp - if ( !req.body.is_temp ) { + if(!req.body.is_temp){ const svc_event = Context.get('services').get('event'); svc_event.emit('user.save_account', { user }); } @@ -481,7 +428,7 @@ module.exports = eggspress(['/signup'], { // return results return res.send({ token: token, - user: { + user:{ username: user.username, uuid: user.uuid, email: user.email, @@ -490,6 +437,6 @@ module.exports = eggspress(['/signup'], { is_temp: (user.password === null && user.email === null), taskbar_items: await get_taskbar_items(user), referral_code, - }, - }); + } + }) }); diff --git a/src/backend/src/services/GetUserService.js b/src/backend/src/services/GetUserService.js index d265f5be4..85e16d5cd 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,19 +135,13 @@ 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 4d1ee7178..44a6f0b5c 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 6c2a53697..1eae5e602 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 -} as const; \ No newline at end of file +}; \ 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 73f80da45..47eb9e0bf 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 -} as const; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/backend/src/services/User.d.ts b/src/backend/src/services/User.d.ts deleted file mode 100644 index 6d0a92487..000000000 --- a/src/backend/src/services/User.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 1423543db..000000000 --- a/src/backend/src/services/UserService.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 adf2b25ce..a0a495570 100644 --- a/src/backend/src/services/UserService.js +++ b/src/backend/src/services/UserService.js @@ -1,47 +1,51 @@ /* * 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' }) }); } @@ -50,15 +54,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. @@ -70,7 +74,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; @@ -84,7 +88,8 @@ 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), @@ -95,24 +100,25 @@ 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; @@ -129,29 +135,20 @@ 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 3e8f1c191..fa4730f33 100644 --- a/src/backend/src/services/auth/Actor.d.ts +++ b/src/backend/src/services/auth/Actor.d.ts @@ -1,17 +1,15 @@ -import { IUser } from '../User'; - export class SystemActorType { get uid(): string; - get_related_type(type_class: unknown): SystemActorType; + get_related_type(type_class: any): SystemActorType; } export class Actor { type: { app: { uid: string } - user: IUser - }; + user: { uuid: string, username: string, email: string, subscription?: (typeof SUB_POLICIES)[keyof typeof SUB_POLICIES]['id'] } + } get uid(): string; clone(): Actor; static get_system_actor(): Actor; - static adapt(actor?: Actor): Actor; + static adapt(actor?: any): 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 deleted file mode 100644 index c8b238d31..000000000 --- a/src/backend/src/services/database/BaseDatabaseAccessService.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -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/SqliteDatabaseAccessService.js b/src/backend/src/services/database/SqliteDatabaseAccessService.js index e7483b0eb..05e0036e4 100644 --- a/src/backend/src/services/database/SqliteDatabaseAccessService.js +++ b/src/backend/src/services/database/SqliteDatabaseAccessService.js @@ -166,9 +166,6 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { [35, [ '0039_add-expireAt-to-kv-store.sql', ]], - [36, [ - '0040_add_user_metadata.sql', - ]], ]; // Database upgrade logic @@ -223,7 +220,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { const stmt = stmts[i] + ';'; try { this.db.exec(stmt); - } catch ( e ) { + } catch( e ) { throw new CompositeError(`failed to apply: ${basename} at line ${i}`, e); } } @@ -234,7 +231,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { await this.run_js_migration_({ filename, contents, }); - } catch ( e ) { + } catch( e ) { throw new CompositeError(`failed to apply: ${basename}`, e); } break; @@ -398,7 +395,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { const fs = require('fs'); const contents = fs.readFileSync(filename, 'utf8'); this.db.exec(contents); - } catch ( err ) { + } catch( err ) { log.error(err.message); } }, @@ -411,7 +408,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService { const [query] = args; const rows = this._read(query, []); log.log(rows); - } catch ( err ) { + } catch( err ) { log.error(err.message); } }, 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 deleted file mode 100644 index cbe94cb5c..000000000 --- a/src/backend/src/services/database/sqlite_setup/0040_add_user_metadata.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `user` ADD COLUMN `metadata` JSON DEFAULT '{}'; \ No newline at end of file diff --git a/src/backend/src/util/expressutil.js b/src/backend/src/util/expressutil.js index 84fee8e34..a0ee952a7 100644 --- a/src/backend/src/util/expressutil.js +++ b/src/backend/src/util/expressutil.js @@ -16,33 +16,34 @@ * 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 ea394a8c1..7a371e52c 100644 --- a/src/dev-center/index.html +++ b/src/dev-center/index.html @@ -31,53 +31,45 @@