mirror of
https://github.com/HeyPuter/puter.git
synced 2026-02-13 17:29:27 -06:00
Revert: commits for user metadata changes (#1887)
This commit is contained in:
@@ -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`'
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
extensions/ExtensionController/.gitignore
vendored
2
extensions/ExtensionController/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*.js
|
||||
*.map
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
const createMethodDecorator = <This>(method: HttpMethod) => {
|
||||
return (path: string, options?: EndpointOptions) => {
|
||||
|
||||
return (target: (req: Request, res: Response)=> void | Promise<void>, _context: ClassMethodDecoratorContext<This, (this:This, ...args:[req: Request, res: Response])=> void | Promise<void>>) => {
|
||||
|
||||
_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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
//@puter priority -1000
|
||||
import * as extensionControllerExports from './ExtensionController.js';
|
||||
|
||||
extension.exports = { ...extensionControllerExports };
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
18
extensions/api.d.ts
vendored
18
extensions/api.d.ts
vendored
@@ -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: <T extends (keyof ServiceNameMap ) | (string & {})>(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<string, unknown> & {
|
||||
json?: boolean
|
||||
noReallyItsJson?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
@@ -61,15 +50,10 @@ interface ServiceNameMap {
|
||||
'meteringService': Pick<MeteringServiceWrapper, 'meteringService'> & 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<string, unknown>,
|
||||
on<T extends unknown[]>(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<T extends `service:${keyof ServiceNameMap}`| (string & {})>(module: T): T extends `service:${infer R extends keyof ServiceNameMap}`
|
||||
? ServiceNameMap[R]
|
||||
: unknown;
|
||||
|
||||
@@ -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 })));
|
||||
});
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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' ) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -16,22 +16,22 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
'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,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -17,20 +17,20 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<void>} 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<Object|null>} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,4 +4,4 @@ export const REGISTERED_USER_FREE = {
|
||||
id: 'user_free',
|
||||
monthUsageAllowance: toMicroCents(0.50),
|
||||
monthlyStorageAllowance: 100 * 1024 * 1024, // 100MiB
|
||||
} as const;
|
||||
};
|
||||
@@ -4,4 +4,4 @@ export const TEMP_USER_FREE = {
|
||||
id: 'temp_free',
|
||||
monthUsageAllowance: toMicroCents(0.25),
|
||||
monthlyStorageAllowance: 100 * 1024 * 1024, // 100MiB
|
||||
} as const;
|
||||
};
|
||||
8
src/backend/src/services/User.d.ts
vendored
8
src/backend/src/services/User.d.ts
vendored
@@ -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<string, unknown> & { hasDevAccountAccess?: boolean }
|
||||
}
|
||||
12
src/backend/src/services/UserService.d.ts
vendored
12
src/backend/src/services/UserService.d.ts
vendored
@@ -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<void>;
|
||||
updateUserMetadata(user: IUser, updatedMetadata: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 = {
|
||||
|
||||
10
src/backend/src/services/auth/Actor.d.ts
vendored
10
src/backend/src/services/auth/Actor.d.ts
vendored
@@ -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;
|
||||
}
|
||||
@@ -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<any>;
|
||||
tryHardRead(query: string, params?: any[]): Promise<any>;
|
||||
requireRead(query: string, params?: any[]): Promise<any>;
|
||||
pread(query: string, params?: any[]): Promise<any>;
|
||||
write(query: string, params?: any[]): Promise<any>;
|
||||
insert(table_name: string, data: Record<string, any>): Promise<any>;
|
||||
batch_write(statements: string[]): any;
|
||||
}
|
||||
|
||||
export class BaseDatabaseAccessService extends BaseService implements IBaseDatabaseAccessService {
|
||||
static DB_WRITE: DBMode;
|
||||
static DB_READ: DBMode;
|
||||
case<T>(choices: Record<string, T>): T;
|
||||
get(): this;
|
||||
read(query: string, params?: any[]): Promise<any>;
|
||||
tryHardRead(query: string, params?: any[]): Promise<any>;
|
||||
requireRead(query: string, params?: any[]): Promise<any>;
|
||||
pread(query: string, params?: any[]): Promise<any>;
|
||||
write(query: string, params?: any[]): Promise<any>;
|
||||
insert(table_name: string, data: Record<string, any>): Promise<any>;
|
||||
batch_write(statements: string[]): any;
|
||||
_gen_insert_sql(table_name: string, data: Record<string, any>): string;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `user` ADD COLUMN `metadata` JSON DEFAULT '{}';
|
||||
@@ -16,33 +16,34 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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,
|
||||
|
||||
@@ -31,53 +31,45 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.polyfills.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
<style>
|
||||
.social-link {
|
||||
.social-link{
|
||||
opacity: 0.7;
|
||||
color: rgb(70, 78, 86);
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
.social-link:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.social-link svg {
|
||||
.social-link svg{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.sidebar-nav-social {
|
||||
.sidebar-nav-social{
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.sidebar-nav-social li {
|
||||
.sidebar-nav-social li{
|
||||
display: inline;
|
||||
padding: 0;
|
||||
padding:0;
|
||||
margin-left: 20px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.sidebar-nav-social li a {
|
||||
.sidebar-nav-social li a{
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
tags {
|
||||
tags{
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.analytics-card {
|
||||
.analytics-card{
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: #f2f4f5eb;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
float: left;
|
||||
float:left;
|
||||
margin-right: 23px;
|
||||
}
|
||||
|
||||
.analytics-card h3 {
|
||||
.analytics-card h3{
|
||||
color: #838383;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -100,368 +92,245 @@
|
||||
<!-- Footer -->
|
||||
<div style="overflow: hidden; position: absolute; bottom: 0; width: 100%;">
|
||||
<ul class="sidebar-nav">
|
||||
<li class="no-hover" style="margin-left:0;"><a href="https://developer.puter.com/"
|
||||
class="link-to-docs" target="_blank">Developer Documentation<img
|
||||
src="img/external-link.svg"></a></li>
|
||||
<li class="no-hover" style="margin-left:0;"><a href="https://developer.puter.com/" class="link-to-docs" target="_blank">Developer Documentation<img src="img/external-link.svg"></a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="sidebar-nav sidebar-nav-social">
|
||||
<li class="no-hover"><a href="https://github.com/HeyPuter/puter" class="social-link"
|
||||
target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8" />
|
||||
</svg></a></li>
|
||||
<li class="no-hover"><a href="https://dsc.gg/puter" class="social-link" target="_blank"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-discord" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" />
|
||||
</svg></a></li>
|
||||
<li class="no-hover"><a href="https://x.com/HeyPuter" class="social-link" target="_blank"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-twitter-x" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z" />
|
||||
</svg></a></li>
|
||||
<li class="no-hover"><a href="https://reddit.com/r/puter" class="social-link" target="_blank"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-reddit" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M6.167 8a.83.83 0 0 0-.83.83c0 .459.372.84.83.831a.831.831 0 0 0 0-1.661m1.843 3.647c.315 0 1.403-.038 1.976-.611a.23.23 0 0 0 0-.306.213.213 0 0 0-.306 0c-.353.363-1.126.487-1.67.487-.545 0-1.308-.124-1.671-.487a.213.213 0 0 0-.306 0 .213.213 0 0 0 0 .306c.564.563 1.652.61 1.977.61zm.992-2.807c0 .458.373.83.831.83s.83-.381.83-.83a.831.831 0 0 0-1.66 0z" />
|
||||
<path
|
||||
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.828-1.165c-.315 0-.602.124-.812.325-.801-.573-1.9-.945-3.121-.993l.534-2.501 1.738.372a.83.83 0 1 0 .83-.869.83.83 0 0 0-.744.468l-1.938-.41a.2.2 0 0 0-.153.028.2.2 0 0 0-.086.134l-.592 2.788c-1.24.038-2.358.41-3.17.992-.21-.2-.496-.324-.81-.324a1.163 1.163 0 0 0-.478 2.224q-.03.17-.029.353c0 1.795 2.091 3.256 4.669 3.256s4.668-1.451 4.668-3.256c0-.114-.01-.238-.029-.353.401-.181.688-.592.688-1.069 0-.65-.525-1.165-1.165-1.165" />
|
||||
</svg></a></li>
|
||||
<li class="no-hover"><a href="https://github.com/HeyPuter/puter" class="social-link" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/> </svg></a></li>
|
||||
<li class="no-hover"><a href="https://dsc.gg/puter" class="social-link" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-discord" viewBox="0 0 16 16"> <path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/> </svg></a></li>
|
||||
<li class="no-hover"><a href="https://x.com/HeyPuter" class="social-link" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16"> <path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/> </svg></a></li>
|
||||
<li class="no-hover"><a href="https://reddit.com/r/puter" class="social-link" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reddit" viewBox="0 0 16 16"> <path d="M6.167 8a.83.83 0 0 0-.83.83c0 .459.372.84.83.831a.831.831 0 0 0 0-1.661m1.843 3.647c.315 0 1.403-.038 1.976-.611a.23.23 0 0 0 0-.306.213.213 0 0 0-.306 0c-.353.363-1.126.487-1.67.487-.545 0-1.308-.124-1.671-.487a.213.213 0 0 0-.306 0 .213.213 0 0 0 0 .306c.564.563 1.652.61 1.977.61zm.992-2.807c0 .458.373.83.831.83s.83-.381.83-.83a.831.831 0 0 0-1.66 0z"/> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.828-1.165c-.315 0-.602.124-.812.325-.801-.573-1.9-.945-3.121-.993l.534-2.501 1.738.372a.83.83 0 1 0 .83-.869.83.83 0 0 0-.744.468l-1.938-.41a.2.2 0 0 0-.153.028.2.2 0 0 0-.086.134l-.592 2.788c-1.24.038-2.358.41-3.17.992-.21-.2-.496-.324-.81-.324a1.163 1.163 0 0 0-.478 2.224q-.03.17-.029.353c0 1.795 2.091 3.256 4.669 3.256s4.668-1.451 4.668-3.256c0-.114-.01-.238-.029-.353.401-.181.688-.592.688-1.069 0-.65-.525-1.165-1.165-1.165"/> </svg></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="main">
|
||||
<!---------------------------------------->
|
||||
<!-- Earn money -->
|
||||
<!---------------------------------------->
|
||||
<dialog id="earn-money">
|
||||
<h3 style="font-size: 30px; margin-top:10px; font-weight: 500;">Developers earn money on Puter!</h3>
|
||||
<p>Follow the steps below to start earning money on Puter:</p>
|
||||
<ol>
|
||||
<li>Publish as many apps as you want on Puter.</li>
|
||||
<li>We automatically review every app continuously. Qualified apps are automatically added to our
|
||||
Incentive Program to earn money.</li>
|
||||
<li>You will earn money every time your approved apps are opened by users.</li>
|
||||
</ol>
|
||||
<span class="close-message" id="earn-money-c2a-close" data-target="#earn-money">✕</span>
|
||||
<hr>
|
||||
<span style="font-size:15px;">Questions? Contact us: <a href="mailto:hi@puter.com"
|
||||
style="outline: none;">hi@puter.com</a></span>
|
||||
<a style="font-size: 14px; float: right; outline: none;" href="https://puter.com/incentive-program-terms"
|
||||
target="_blank">Incentive Program Terms</a>
|
||||
</dialog>
|
||||
<!---------------------------------------->
|
||||
<!-- Earn money -->
|
||||
<!---------------------------------------->
|
||||
<dialog id="earn-money">
|
||||
<h3 style="font-size: 30px; margin-top:10px; font-weight: 500;">Developers earn money on Puter!</h3>
|
||||
<p>Follow the steps below to start earning money on Puter:</p>
|
||||
<ol>
|
||||
<li>Publish as many apps as you want on Puter.</li>
|
||||
<li>We automatically review every app continuously. Qualified apps are automatically added to our Incentive Program to earn money.</li>
|
||||
<li>You will earn money every time your approved apps are opened by users.</li>
|
||||
</ol>
|
||||
<span class="close-message" id="earn-money-c2a-close" data-target="#earn-money">✕</span>
|
||||
<hr>
|
||||
<span style="font-size:15px;">Questions? Contact us: <a href="mailto:hi@puter.com" style="outline: none;">hi@puter.com</a></span>
|
||||
<a style="font-size: 14px; float: right; outline: none;" href="https://puter.com/incentive-program-terms" target="_blank">Incentive Program Terms</a>
|
||||
</dialog>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- Dev Incentive Program -->
|
||||
<!---------------------------------------->
|
||||
<section id="join-incentive-program">
|
||||
<section id="jip-form">
|
||||
<h1 style="font-weight: 400;">Great News!</h1>
|
||||
<p>You are approved to join the Puter Incentive Program: A revolutionary, invite-only program to earn
|
||||
money every time your apps are opened!</p>
|
||||
<p>Please use the following form to join the program.</p>
|
||||
<form style="clear:both;">
|
||||
<div>
|
||||
<div class="error" id="jip-error" style="width: 390px;"></div>
|
||||
|
||||
<div style="margin-bottom: 10px; overflow:hidden;">
|
||||
<div style="width: 200px; float:left;">
|
||||
<label for="jip-first-name">First Name</label>
|
||||
<input type="text" id="jip-first-name" placeholder="">
|
||||
</div>
|
||||
|
||||
<div style="width: 200px; float:left; margin-left:10px;">
|
||||
<label for="jip-last-name">Last Name</label>
|
||||
<input type="text" id="jip-last-name" placeholder="">
|
||||
</div>
|
||||
<!---------------------------------------->
|
||||
<!-- Dev Incentive Program -->
|
||||
<!---------------------------------------->
|
||||
<section id="join-incentive-program">
|
||||
<section id="jip-form">
|
||||
<h1 style="font-weight: 400;">Great News!</h1>
|
||||
<p>You are approved to join the Puter Incentive Program: A revolutionary, invite-only program to earn money every time your apps are opened!</p>
|
||||
<p>Please use the following form to join the program.</p>
|
||||
<form style="clear:both;">
|
||||
<div>
|
||||
<div class="error" id="jip-error" style="width: 390px;"></div>
|
||||
|
||||
<div style="margin-bottom: 10px; overflow:hidden;">
|
||||
<div style="width: 200px; float:left;">
|
||||
<label for="jip-first-name">First Name</label>
|
||||
<input type="text" id="jip-first-name" placeholder="">
|
||||
</div>
|
||||
|
||||
<div style="clear: both; margin-top: 20px; width: 410px;">
|
||||
<label for="jip-paypal">Paypal email address for receiving your payouts</label>
|
||||
<input type="text" id="jip-paypal">
|
||||
|
||||
<div style="width: 200px; float:left; margin-left:10px;">
|
||||
<label for="jip-last-name">Last Name</label>
|
||||
<input type="text" id="jip-last-name" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="clear: both; margin-top: 20px; width: 410px;">
|
||||
<label for="jip-paypal">Paypal email address for receiving your payouts</label>
|
||||
<input type="text" id="jip-paypal">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="ip-terms-notice">By clicking Join Now, you agree to our <a
|
||||
href="https://puter.com/incentive-program-terms" target="_blank">Incentive Program
|
||||
Terms</a>.</p>
|
||||
<button type="button" class="jip-submit-btn button button-large button-primary">Join Now</button>
|
||||
</form>
|
||||
</section>
|
||||
<section id="jip-success">
|
||||
<h1>🎉 Congratulations!</h1>
|
||||
<p>You have successfully joined the Puter Incentive Program. You will start earning money from your
|
||||
eligible apps.</p>
|
||||
<p>Please do not hesitate to contact us at <a href="mailto:hey@puter.com">hey@puter.com</a> should you
|
||||
have any questions.</p>
|
||||
<span class="close-message" data-target="#join-incentive-program">✕</span>
|
||||
</section>
|
||||
<p class="ip-terms-notice">By clicking Join Now, you agree to our <a href="https://puter.com/incentive-program-terms" target="_blank">Incentive Program Terms</a>.</p>
|
||||
<button type="button" class="jip-submit-btn button button-large button-primary">Join Now</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- Payout Method -->
|
||||
<!---------------------------------------->
|
||||
<section id="tab-payout-method" style="display:none;">
|
||||
<h1>Payout Method</h1>
|
||||
<div style="overflow: hidden;">
|
||||
<img src="./img/paypal.svg" style="float:left; width: 40px; height: 50px;"><span
|
||||
id="payout-method-email"></span>
|
||||
</div>
|
||||
<p style="font-size:14px; margin-top:20px;"><strong>Please note:</strong> every month, you will receive your
|
||||
earnings from the previous month. The payment is usually processed within the first seven business days
|
||||
of the new month. Please do not hesitate to contact us at <a
|
||||
href="mailto:hey@puter.com">hey@puter.com</a> should you have any questions.</p>
|
||||
<section id="jip-success">
|
||||
<h1>🎉 Congratulations!</h1>
|
||||
<p>You have successfully joined the Puter Incentive Program. You will start earning money from your eligible apps.</p>
|
||||
<p>Please do not hesitate to contact us at <a href="mailto:hey@puter.com">hey@puter.com</a> should you have any questions.</p>
|
||||
<span class="close-message" data-target="#join-incentive-program">✕</span>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- No Apps Messaage -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-apps-notice" style="display:none;">
|
||||
<img src="./img/apps-black.svg" style="width: 64px; opacity: 0.12;">
|
||||
<p style="color: #606062;">You haven't created any apps yet.</p>
|
||||
<button class="create-an-app-btn button button-primary"><img src="./img/plus.svg">Create an App</button>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- Payout Method -->
|
||||
<!---------------------------------------->
|
||||
<section id="tab-payout-method" style="display:none;">
|
||||
<h1>Payout Method</h1>
|
||||
<div style="overflow: hidden;">
|
||||
<img src="./img/paypal.svg" style="float:left; width: 40px; height: 50px;"><span id="payout-method-email"></span>
|
||||
</div>
|
||||
<p style="font-size:14px; margin-top:20px;"><strong>Please note:</strong> every month, you will receive your earnings from the previous month. The payment is usually processed within the first seven business days of the new month. Please do not hesitate to contact us at <a href="mailto:hey@puter.com">hey@puter.com</a> should you have any questions.</p>
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- No Workers Message -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-workers-notice" style="display:none;">
|
||||
<img src="./img/workers-placeholder.svg"
|
||||
style="width: 64px; height: 64px; opacity: 0.62; filter: grayscale(100%); transform: rotate(-20deg);">
|
||||
<p style="color: #606062;">You haven't created any workers yet.</p>
|
||||
<button class="create-a-worker-btn button button-primary"><img src="./img/plus.svg">Create a Worker</button>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- No Apps Messaage -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-apps-notice" style="display:none;">
|
||||
<img src="./img/apps-black.svg" style="width: 64px; opacity: 0.12;">
|
||||
<p style="color: #606062;">You haven't created any apps yet.</p>
|
||||
<button class="create-an-app-btn button button-primary"><img src="./img/plus.svg">Create an App</button>
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- No Websites Message -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-websites-notice" style="display:none;">
|
||||
<img src="./img/websites-placeholder.svg"
|
||||
style="width: 64px; height: 64px; opacity: 0.22; filter: grayscale(100%);">
|
||||
<p style="color: #606062;">You haven't created any websites yet.</p>
|
||||
<button class="create-a-website-btn button button-primary"><img src="./img/plus.svg">Create a
|
||||
Website</button>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- Edit App -->
|
||||
<!---------------------------------------->
|
||||
<section id="edit-app" style="margin-bottom: 100px;">
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- No Workers Message -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-workers-notice" style="display:none;">
|
||||
<img src="./img/workers-placeholder.svg" style="width: 64px; height: 64px; opacity: 0.62; filter: grayscale(100%); transform: rotate(-20deg);">
|
||||
<p style="color: #606062;">You haven't created any workers yet.</p>
|
||||
<button class="create-a-worker-btn button button-primary"><img src="./img/plus.svg">Create a Worker</button>
|
||||
</section>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- Insta-Deploy Modal -->
|
||||
<!---------------------------------------->
|
||||
<dialog class="insta-deploy-modal">
|
||||
<p>Deploy <strong class="insta-deploy-item-name"></strong> to:</p>
|
||||
<div style="overflow: hidden;">
|
||||
<div class="insta-deploy-to-new-app">New App</div>
|
||||
<div class="insta-deploy-to-existing-app">An Existing App</div>
|
||||
</div>
|
||||
<span class="insta-deploy-cancel">Cancel</span>
|
||||
</dialog>
|
||||
<!---------------------------------------->
|
||||
<!-- No Websites Message -->
|
||||
<!---------------------------------------->
|
||||
<section id="no-websites-notice" style="display:none;">
|
||||
<img src="./img/websites-placeholder.svg" style="width: 64px; height: 64px; opacity: 0.22; filter: grayscale(100%);">
|
||||
<p style="color: #606062;">You haven't created any websites yet.</p>
|
||||
<button class="create-a-website-btn button button-primary"><img src="./img/plus.svg">Create a Website</button>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- Edit App -->
|
||||
<!---------------------------------------->
|
||||
<section id="edit-app" style="margin-bottom: 100px;">
|
||||
</section>
|
||||
|
||||
<dialog class="insta-deploy-existing-app-select">
|
||||
<span class="insta-deploy-existing-app-back">Back</span>
|
||||
<p>Select app to deploy to:</p>
|
||||
<div class="insta-deploy-existing-app-list"></div>
|
||||
<button style="margin-top: 10px;"
|
||||
class="button button-primary button-block disabled insta-deploy-existing-app-deploy-btn">Deploy</button>
|
||||
<div class="insta-deploy-cancel">Cancel</div>
|
||||
</dialog>
|
||||
<!---------------------------------------->
|
||||
<!-- Insta-Deploy Modal -->
|
||||
<!---------------------------------------->
|
||||
<dialog class="insta-deploy-modal">
|
||||
<p>Deploy <strong class="insta-deploy-item-name"></strong> to:</p>
|
||||
<div style="overflow: hidden;">
|
||||
<div class="insta-deploy-to-new-app">New App</div>
|
||||
<div class="insta-deploy-to-existing-app">An Existing App</div>
|
||||
</div>
|
||||
<span class="insta-deploy-cancel">Cancel</span>
|
||||
</dialog>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- App List -->
|
||||
<!---------------------------------------->
|
||||
<section id="app-list">
|
||||
<div class="app-list-nav">
|
||||
<h1 class="my-apps-title">My Apps<span class="app-count"></span></h1>
|
||||
<button class="setup-account-btn button button-secondary" style="float:right; margin-bottom: 10px;">Open
|
||||
Payments Dev Account</button>
|
||||
<button class="create-an-app-btn button button-primary" style="float:right; margin-bottom: 10px;">New
|
||||
App</button>
|
||||
</div>
|
||||
<dialog class="insta-deploy-existing-app-select">
|
||||
<span class="insta-deploy-existing-app-back">Back</span>
|
||||
<p>Select app to deploy to:</p>
|
||||
<div class="insta-deploy-existing-app-list"></div>
|
||||
<button style="margin-top: 10px;" class="button button-primary button-block disabled insta-deploy-existing-app-deploy-btn">Deploy</button>
|
||||
<div class="insta-deploy-cancel">Cancel</div>
|
||||
</dialog>
|
||||
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-apps"
|
||||
placeholder="Search apps">
|
||||
<img class="search-clear search-clear-apps" src="./img/close.svg">
|
||||
</div>
|
||||
<!---------------------------------------->
|
||||
<!-- App List -->
|
||||
<!---------------------------------------->
|
||||
<section id="app-list">
|
||||
<div class="app-list-nav">
|
||||
<h1 class="my-apps-title">My Apps<span class="app-count"></span></h1>
|
||||
<button class="create-an-app-btn button button-primary"><img src="./img/plus.svg" style="width: 25px; height: 25px;">New App</button>
|
||||
</div>
|
||||
|
||||
<button class="button button-danger disabled delete-apps-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-app-list" title="Refresh"><svg class="refresh-icon"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2">
|
||||
<g stroke-width="2" transform="translate(0.5, 0.5)">
|
||||
<path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444"
|
||||
stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter">
|
||||
</path>
|
||||
<polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444"
|
||||
stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter">
|
||||
</polyline>
|
||||
<path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none"
|
||||
stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt"
|
||||
stroke-linejoin="miter"></path>
|
||||
<polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none"
|
||||
stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"
|
||||
stroke-linejoin="miter"></polyline>
|
||||
</g>
|
||||
</svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="app-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-apps"
|
||||
style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">App<span
|
||||
class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-users" data-column="user_count">Users<span
|
||||
class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-opens" data-column="open_count">Opens<span
|
||||
class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span
|
||||
class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-apps" placeholder="Search apps">
|
||||
<img class="search-clear search-clear-apps" src="./img/close.svg">
|
||||
</div>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<button class="button button-danger disabled delete-apps-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-app-list" title="Refresh"><svg class="refresh-icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2"><g stroke-width="2" transform="translate(0.5, 0.5)"><path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline><path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline></g></svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="app-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-apps" style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">App<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-users" data-column="user_count">Users<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-opens" data-column="open_count">Opens<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- Worker List -->
|
||||
<!---------------------------------------->
|
||||
<section id="worker-list">
|
||||
<div class="worker-list-nav">
|
||||
<h1 class="my-workers-title">My Workers<span class="worker-count"></span></h1>
|
||||
<button class="create-a-worker-btn button button-primary"><img src="./img/plus.svg"
|
||||
style="width: 25px; height: 25px;"> New Worker</button>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-workers"
|
||||
placeholder="Search workers">
|
||||
<img class="search-clear search-clear-workers" src="./img/close.svg">
|
||||
</div>
|
||||
<!---------------------------------------->
|
||||
<!-- Worker List -->
|
||||
<!---------------------------------------->
|
||||
<section id="worker-list">
|
||||
<div class="worker-list-nav">
|
||||
<h1 class="my-workers-title">My Workers<span class="worker-count"></span></h1>
|
||||
<button class="create-a-worker-btn button button-primary"><img src="./img/plus.svg" style="width: 25px; height: 25px;"> New Worker</button>
|
||||
</div>
|
||||
|
||||
<button class="button button-danger disabled delete-workers-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-worker-list" title="Refresh"><svg class="refresh-icon"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2">
|
||||
<g stroke-width="2" transform="translate(0.5, 0.5)">
|
||||
<path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444"
|
||||
stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter">
|
||||
</path>
|
||||
<polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444"
|
||||
stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter">
|
||||
</polyline>
|
||||
<path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none"
|
||||
stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt"
|
||||
stroke-linejoin="miter"></path>
|
||||
<polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none"
|
||||
stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"
|
||||
stroke-linejoin="miter"></polyline>
|
||||
</g>
|
||||
</svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="worker-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-workers"
|
||||
style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">
|
||||
Worker<span class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-file" data-column="file_path">File<span
|
||||
class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span
|
||||
class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-workers" placeholder="Search workers">
|
||||
<img class="search-clear search-clear-workers" src="./img/close.svg">
|
||||
</div>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- Websites List -->
|
||||
<!---------------------------------------->
|
||||
<section id="website-list">
|
||||
<div class="website-list-nav">
|
||||
<h1 class="my-websites-title">My Websites<span class="website-count"></span></h1>
|
||||
<button class="create-a-website-btn button button-primary"><img src="./img/plus.svg"
|
||||
style="width: 25px; height: 25px;">New Website</button>
|
||||
</div>
|
||||
<button class="button button-danger disabled delete-workers-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-worker-list" title="Refresh"><svg class="refresh-icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2"><g stroke-width="2" transform="translate(0.5, 0.5)"><path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline><path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline></g></svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="worker-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-workers" style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">Worker<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-file" data-column="file_path">File<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-websites"
|
||||
placeholder="Search websites">
|
||||
<img class="search-clear search-clear-websites" src="./img/close.svg">
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- Websites List -->
|
||||
<!---------------------------------------->
|
||||
<section id="website-list">
|
||||
<div class="website-list-nav">
|
||||
<h1 class="my-websites-title">My Websites<span class="website-count"></span></h1>
|
||||
<button class="create-a-website-btn button button-primary"><img src="./img/plus.svg" style="width: 25px; height: 25px;">New Website</button>
|
||||
</div>
|
||||
|
||||
<button class="button button-danger disabled delete-websites-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-website-list" style="width:40px; padding: 10px; float: right; margin-right: 10px;"
|
||||
title="Refresh"><svg class="refresh-icon" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px"
|
||||
viewBox="0 0 32 32" stroke-width="2">
|
||||
<g stroke-width="2" transform="translate(0.5, 0.5)">
|
||||
<path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444"
|
||||
stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter">
|
||||
</path>
|
||||
<polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444"
|
||||
stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter">
|
||||
</polyline>
|
||||
<path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none"
|
||||
stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt"
|
||||
stroke-linejoin="miter"></path>
|
||||
<polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none"
|
||||
stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"
|
||||
stroke-linejoin="miter"></polyline>
|
||||
</g>
|
||||
</svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="website-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-websites"
|
||||
style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">
|
||||
Website<span class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-root-dir" data-column="root_dir">Connected Directory<span
|
||||
class="sort-arrow sort-arrow-desc">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span
|
||||
class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span
|
||||
class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div class="search-container">
|
||||
<input style="background-image:url(./img/magnifier-outline.svg);" class="search search-websites" placeholder="Search websites">
|
||||
<img class="search-clear search-clear-websites" src="./img/close.svg">
|
||||
</div>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<button class="button button-danger disabled delete-websites-btn" style="float:right;">Delete</button>
|
||||
<button class="refresh-website-list" style="width:40px; padding: 10px; float: right; margin-right: 10px;" title="Refresh"><svg class="refresh-icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" stroke-width="2"><g stroke-width="2" transform="translate(0.5, 0.5)"><path data-cap="butt" d="M29.382,9.217A15,15,0,0,0,1,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline points="28.383 1.22 29.383 9.22 21.383 8.22" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline><path data-cap="butt" data-color="color-2" d="M2.618,22.783A15,15,0,0,0,31,16" fill="none" stroke="#444444" stroke-miterlimit="10" stroke-width="2" stroke-linecap="butt" stroke-linejoin="miter"></path><polyline data-color="color-2" points="3.617 30.78 2.617 22.78 10.617 23.78" fill="none" stroke="#444444" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></polyline></g></svg></button>
|
||||
<div style="overflow-x: auto; clear: both;">
|
||||
<table class="table" id="website-list-table">
|
||||
<thead class="disable-user-select">
|
||||
<tr>
|
||||
<th><input type="checkbox" class="select-all-websites" style="width: 15px; height: 20px; margin-left:3px;"></th>
|
||||
<th class="sort th-name" data-column="name" style="padding-left: 10px !important;">Website<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-root-dir" data-column="root_dir">Connected Directory<span class="sort-arrow sort-arrow-desc">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th class="sort th-created sorted" data-column="created_at">Created<span class="sort-arrow sort-arrow-desc" style="display:inline;">▼</span><span class="sort-arrow sort-arrow-asc">▲</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
url: http://puter.localhost:4100/
|
||||
username: admin
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0IjoicyIsInYiOiIwLjAuMCIsInUiOiI3Vjlkazlxd1N0R1NjLzdFakYraXN3PT0iLCJ1dSI6ImtNa0NYbTBIU01DS2w5K2lNSE10dXc9PSIsImlhdCI6MTc1Nzc5MTcyOX0.s82vH4IKVQUIFTKW7iHzSOlTlDkqmvEzHj4fPpYDi1w
|
||||
mountpoints:
|
||||
- path: /
|
||||
provider: puterfs
|
||||
- path: /admin/tmp
|
||||
provider: memoryfs
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"target": "ES2022",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"rootDir": ".",
|
||||
@@ -8,7 +8,7 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
@@ -17,6 +17,5 @@
|
||||
"**/tests/**",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"volatile"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user