mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 15:37:12 +00:00
feat: ui for dev account + util decorators for controllers in extension (#1864)
* fix: IPC trigger for dev account setup + util decorators for ext controllers * feat: allow extension to bypass json * fix: ts building in volatile
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 }, (req, res) => {
|
||||
extension.get('/user-count', { noauth: true, mw: [] }, (req, res) => {
|
||||
const [count] = await ext.db.read(
|
||||
'SELECT COUNT(*) as c FROM `user`'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.js
|
||||
*.map
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "extensionController",
|
||||
"priority": -1000,
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^24.9.1",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"stripe": "^19.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import type { EndpointOptions, HttpMethod } from '../../api.d.ts';
|
||||
|
||||
/**
|
||||
* Controller decorator to set prefix on prototype and register routes on instantiation
|
||||
*/
|
||||
export const Controller = (prefix: string): ClassDecorator => {
|
||||
return (target: Function) => {
|
||||
target.prototype.__controllerPrefix = prefix;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Method decorator factory that collects route metadata
|
||||
*/
|
||||
interface RouteMeta {
|
||||
method: HttpMethod;
|
||||
path: string;
|
||||
options?: EndpointOptions | undefined;
|
||||
handler: (req: Request, res: Response)=> void | Promise<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//@puter priority -1000
|
||||
import * as extensionControllerExports from './ExtensionController.js';
|
||||
|
||||
extension.exports = { ...extensionControllerExports };
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
"rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"types": [],
|
||||
// For nodejs:
|
||||
// "lib": ["esnext"],
|
||||
// "types": ["node"],
|
||||
// and npm install -D @types/node
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
}
|
||||
}
|
||||
Vendored
+16
-1
@@ -1,17 +1,23 @@
|
||||
import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js';
|
||||
import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts';
|
||||
import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.ts';
|
||||
import type { MeteringServiceWrapper } from '@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs';
|
||||
import type { DBKVStore } from '@heyputer/backend/src/services/repositories/DBKVStore/DBKVStore.ts';
|
||||
import type { SUService } from '@heyputer/backend/src/services/SUService.js';
|
||||
import type { IUser } from '@heyputer/backend/src/services/User.js';
|
||||
import type { UserService } from '@heyputer/backend/src/services/UserService.d.ts';
|
||||
import type { RequestHandler } from 'express';
|
||||
import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js';
|
||||
import type helpers from '../src/backend/src/helpers.js';
|
||||
import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
services: { get: <T extends (keyof ServiceNameMap ) | (string & {})>(string: T)=> T extends keyof ServiceNameMap ? ServiceNameMap[T] : unknown }
|
||||
actor: Actor
|
||||
actor: Actor,
|
||||
/** @deprecated use actor instead */
|
||||
user: IUser
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +26,11 @@ interface EndpointOptions {
|
||||
allowedMethods?: string[]
|
||||
subdomain?: string
|
||||
noauth?: boolean
|
||||
mw?: RequestHandler[]
|
||||
otherOpts?: Record<string, unknown> & {
|
||||
json?: boolean
|
||||
noReallyItsJson?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
@@ -50,10 +61,14 @@ 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>,
|
||||
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;
|
||||
|
||||
+19
-19
@@ -47,7 +47,7 @@ const whoami_common = ({ is_user, user }) => {
|
||||
epoch = new Date(user.last_activity_ts).getTime();
|
||||
// round to 1 decimal place
|
||||
epoch = Math.round(epoch / 1000);
|
||||
} catch (e) {
|
||||
} catch ( e ) {
|
||||
console.error('Error parsing last_activity_ts', e);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ extension.get('/whoami', { subdomain: 'api' }, async (req, res, next) => {
|
||||
referral_code: req.user.referral_code,
|
||||
otp: !! req.user.otp_enabled,
|
||||
human_readable_age: timeago.format(new Date(req.user.timestamp)),
|
||||
hasDevAccountAccess: !! req.user.metadata?.hasDevAccountAccess,
|
||||
...(req.new_token ? { token: req.token } : {}),
|
||||
};
|
||||
|
||||
@@ -148,37 +149,35 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
|
||||
let desktop_items = [];
|
||||
|
||||
// check if user asked for desktop items
|
||||
if(req.query.return_desktop_items === 1 || req.query.return_desktop_items === '1' || req.query.return_desktop_items === 'true'){
|
||||
if ( req.query.return_desktop_items === 1 || req.query.return_desktop_items === '1' || req.query.return_desktop_items === 'true' ){
|
||||
// by cached desktop id
|
||||
if(req.user.desktop_id){
|
||||
if ( req.user.desktop_id ){
|
||||
// TODO: Check if used anywhere, maybe remove
|
||||
// eslint-disable-next-line no-undef
|
||||
desktop_items = await db.read(
|
||||
`SELECT * FROM fsentries
|
||||
desktop_items = await db.read(`SELECT * FROM fsentries
|
||||
WHERE user_id = ? AND parent_uid = ?`,
|
||||
[req.user.id, await id2uuid(req.user.desktop_id)]
|
||||
)
|
||||
[req.user.id, await id2uuid(req.user.desktop_id)]);
|
||||
}
|
||||
// by desktop path
|
||||
else{
|
||||
desktop_items = await get_descendants(req.user.username +'/Desktop', req.user, 1, true);
|
||||
else {
|
||||
desktop_items = await get_descendants(req.user.username + '/Desktop', req.user, 1, true);
|
||||
}
|
||||
|
||||
// clean up desktop items and add some extra information
|
||||
if(desktop_items.length > 0){
|
||||
if(desktop_items.length > 0){
|
||||
for (let i = 0; i < desktop_items.length; i++) {
|
||||
if(desktop_items[i].id !== null){
|
||||
if ( desktop_items.length > 0 ){
|
||||
if ( desktop_items.length > 0 ){
|
||||
for ( let i = 0; i < desktop_items.length; i++ ) {
|
||||
if ( desktop_items[i].id !== null ){
|
||||
// suggested_apps for files
|
||||
if(!desktop_items[i].is_dir){
|
||||
desktop_items[i].suggested_apps = await suggest_app_for_fsentry(desktop_items[i], {user: req.user});
|
||||
if ( !desktop_items[i].is_dir ){
|
||||
desktop_items[i].suggested_apps = await suggest_app_for_fsentry(desktop_items[i], { user: req.user });
|
||||
}
|
||||
// is_shared
|
||||
desktop_items[i].is_shared = await is_shared_with_anyone(desktop_items[i].id);
|
||||
|
||||
// associated_app
|
||||
if(desktop_items[i].associated_app_id){
|
||||
const app = await get_app({id: desktop_items[i].associated_app_id})
|
||||
if ( desktop_items[i].associated_app_id ){
|
||||
const app = await get_app({ id: desktop_items[i].associated_app_id });
|
||||
|
||||
// remove some privileged information
|
||||
delete app.id;
|
||||
@@ -189,7 +188,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
|
||||
// add to array
|
||||
desktop_items[i].associated_app = app;
|
||||
|
||||
}else{
|
||||
} else {
|
||||
desktop_items[i].associated_app = {};
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
|
||||
delete desktop_items[i].id;
|
||||
delete desktop_items[i].user_id;
|
||||
delete desktop_items[i].bucket;
|
||||
desktop_items[i].path = _path.join('/', req.user.username, desktop_items[i].name)
|
||||
desktop_items[i].path = _path.join('/', req.user.username, desktop_items[i].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,5 +220,6 @@ extension.post('/whoami', { subdomain: 'api' }, async (req, res) => {
|
||||
taskbar_items: await get_taskbar_items(req.user),
|
||||
desktop_items: desktop_items,
|
||||
referral_code: req.user.referral_code,
|
||||
hasDevAccountAccess: !! req.user.metadata?.hasDevAccountAccess,
|
||||
}, whoami_common({ is_user, user: req.user })));
|
||||
});
|
||||
|
||||
+102
-62
@@ -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,59 +114,53 @@ class Extension extends AdvancedBase {
|
||||
/**
|
||||
* This will get a database instance from the default service.
|
||||
*/
|
||||
get db () {
|
||||
get db() {
|
||||
const db = this.service.values.get('db');
|
||||
if ( ! db ) {
|
||||
throw new Error(
|
||||
'extension tried to access database before it was ' +
|
||||
'initialized'
|
||||
);
|
||||
throw new Error('extension tried to access database before it was ' +
|
||||
'initialized');
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
get services () {
|
||||
get services() {
|
||||
const services = this.service.values.get('services');
|
||||
if ( ! services ) {
|
||||
throw new Error(
|
||||
'extension tried to access "services" before it was ' +
|
||||
'initialized'
|
||||
);
|
||||
throw new Error('extension tried to access "services" before it was ' +
|
||||
'initialized');
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
get log_context () {
|
||||
get log_context() {
|
||||
const log_context = this.service.values.get('log_context');
|
||||
if ( ! log_context ) {
|
||||
throw new Error(
|
||||
'extension tried to access "log_context" before it was ' +
|
||||
'initialized'
|
||||
);
|
||||
throw new Error('extension tried to access "log_context" before it was ' +
|
||||
'initialized');
|
||||
}
|
||||
return log_context;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register anonymous or named data to a particular type/category.
|
||||
* @param {string} typeKey Type of data being registered
|
||||
* @param {string} [key] Key of data being registered
|
||||
* @param {any} data The data to be registered
|
||||
*/
|
||||
register (typeKey, keyOrData, data) {
|
||||
register(typeKey, keyOrData, data) {
|
||||
if ( ! this.registry_[typeKey] ) {
|
||||
this.registry_[typeKey] = {
|
||||
named: {},
|
||||
anonymous: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const typeRegistry = this.registry_[typeKey];
|
||||
|
||||
|
||||
if ( arguments.length <= 1 ) {
|
||||
throw new Error('you must specify what to register');
|
||||
}
|
||||
|
||||
|
||||
if ( arguments.length === 2 ) {
|
||||
data = keyOrData;
|
||||
if ( Array.isArray(data) ) {
|
||||
@@ -178,28 +172,28 @@ class Extension extends AdvancedBase {
|
||||
typeRegistry.anonymous.push(data);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const key = keyOrData;
|
||||
typeRegistry.named[key] = data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Alias for .register()
|
||||
* @param {string} typeKey Type of data being registered
|
||||
* @param {string} [key] Key of data being registered
|
||||
* @param {any} data The data to be registered
|
||||
*/
|
||||
reg (...a) {
|
||||
reg(...a) {
|
||||
this.register(...a);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This will create a GET endpoint on the default service.
|
||||
* @param {*} path - route for the endpoint
|
||||
* @param {*} handler - function to handle the endpoint
|
||||
* @param {*} options - options like noauth (bool) and mw (array)
|
||||
*/
|
||||
get (path, handler, options) {
|
||||
get(path, handler, options) {
|
||||
// this extension will have a default service
|
||||
this.ensure_service_();
|
||||
|
||||
@@ -221,7 +215,7 @@ class Extension extends AdvancedBase {
|
||||
* @param {*} handler - function to handle the endpoint
|
||||
* @param {*} options - options like noauth (bool) and mw (array)
|
||||
*/
|
||||
post (path, handler, options) {
|
||||
post(path, handler, options) {
|
||||
// this extension will have a default service
|
||||
this.ensure_service_();
|
||||
|
||||
@@ -236,8 +230,52 @@ class Extension extends AdvancedBase {
|
||||
methods: ['POST'],
|
||||
});
|
||||
}
|
||||
|
||||
use (...args) {
|
||||
|
||||
/**
|
||||
* This will create a DELETE endpoint on the default service.
|
||||
* @param {*} path - route for the endpoint
|
||||
* @param {*} handler - function to handle the endpoint
|
||||
* @param {*} options - options like noauth (bool) and mw (array)
|
||||
*/
|
||||
put(path, handler, options) {
|
||||
// this extension will have a default service
|
||||
this.ensure_service_();
|
||||
|
||||
// handler and options may be flipped
|
||||
if ( typeof handler === 'object' ) {
|
||||
[handler, options] = [options, handler];
|
||||
}
|
||||
if ( ! options ) options = {};
|
||||
|
||||
this.service.register_route_handler_(path, handler, {
|
||||
...options,
|
||||
methods: ['PUT'],
|
||||
});
|
||||
}
|
||||
/**
|
||||
* This will create a DELETE endpoint on the default service.
|
||||
* @param {*} path - route for the endpoint
|
||||
* @param {*} handler - function to handle the endpoint
|
||||
* @param {*} options - options like noauth (bool) and mw (array)
|
||||
*/
|
||||
|
||||
delete(path, handler, options) {
|
||||
// this extension will have a default service
|
||||
this.ensure_service_();
|
||||
|
||||
// handler and options may be flipped
|
||||
if ( typeof handler === 'object' ) {
|
||||
[handler, options] = [options, handler];
|
||||
}
|
||||
if ( ! options ) options = {};
|
||||
|
||||
this.service.register_route_handler_(path, handler, {
|
||||
...options,
|
||||
methods: ['DELETE'],
|
||||
});
|
||||
}
|
||||
|
||||
use(...args) {
|
||||
this.ensure_service_();
|
||||
this.service.expressThings_.push({
|
||||
type: 'router',
|
||||
@@ -246,7 +284,7 @@ class Extension extends AdvancedBase {
|
||||
}
|
||||
|
||||
get preinit() {
|
||||
return (function (callback) {
|
||||
return (function(callback) {
|
||||
this.on('preinit', callback);
|
||||
}).bind(this);
|
||||
}
|
||||
@@ -257,7 +295,8 @@ class Extension extends AdvancedBase {
|
||||
});
|
||||
}
|
||||
if ( callback === null ) {
|
||||
this.only_one_preinit_fn = () => {};
|
||||
this.only_one_preinit_fn = () => {
|
||||
};
|
||||
}
|
||||
this.only_one_preinit_fn = callback;
|
||||
}
|
||||
@@ -267,19 +306,20 @@ class Extension extends AdvancedBase {
|
||||
this.on('init', callback);
|
||||
}).bind(this);
|
||||
}
|
||||
set init (callback) {
|
||||
set init(callback) {
|
||||
if ( this.only_one_init_fn === null ) {
|
||||
this.on('init', (...a) => {
|
||||
this.only_one_init_fn(...a);
|
||||
});
|
||||
}
|
||||
if ( callback === null ) {
|
||||
this.only_one_init_fn = () => {};
|
||||
this.only_one_init_fn = () => {
|
||||
};
|
||||
}
|
||||
this.only_one_init_fn = callback;
|
||||
}
|
||||
|
||||
get console () {
|
||||
get console() {
|
||||
const extensionConsole = Object.create(console);
|
||||
const logfn = level => (...a) => {
|
||||
let svc_log;
|
||||
@@ -317,10 +357,10 @@ class Extension extends AdvancedBase {
|
||||
* This method will create the "default service" for an extension.
|
||||
* This is specifically for Puter extensions that do not define their
|
||||
* own service classes.
|
||||
*
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
ensure_service_ () {
|
||||
ensure_service_() {
|
||||
if ( this.service ) {
|
||||
return;
|
||||
}
|
||||
@@ -333,4 +373,4 @@ class Extension extends AdvancedBase {
|
||||
|
||||
module.exports = {
|
||||
Extension,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,8 +64,9 @@ class ExtensionServiceState extends AdvancedBase {
|
||||
route: path,
|
||||
handler: handler,
|
||||
...(options.subdomain ? { subdomain: options.subdomain } : {}),
|
||||
otherOpts: options.otherOpts || {},
|
||||
});
|
||||
|
||||
|
||||
this.expressThings_.push({ type: 'endpoint', value: endpoint });
|
||||
}
|
||||
}
|
||||
@@ -76,15 +77,15 @@ class ExtensionServiceState extends AdvancedBase {
|
||||
* provide a default service for extensions.
|
||||
*/
|
||||
class ExtensionService extends BaseService {
|
||||
_construct () {
|
||||
_construct() {
|
||||
this.expressThings_ = [];
|
||||
}
|
||||
async _init (args) {
|
||||
async _init(args) {
|
||||
this.state = args.state;
|
||||
|
||||
|
||||
this.state.values.set('services', this.services);
|
||||
this.state.values.set('log_context', this.services.get('log-service').create(
|
||||
this.state.extension.name));
|
||||
this.state.extension.name));
|
||||
|
||||
// Create database access object for extension
|
||||
const db = this.services.get('database').get(DB_WRITE, 'extension');
|
||||
@@ -113,20 +114,20 @@ class ExtensionService extends BaseService {
|
||||
// Propagate all events from extension to Puter's event bus
|
||||
this.state.extension.on_all(async (key, data, meta) => {
|
||||
if ( meta.from_outside_of_extension ) return;
|
||||
|
||||
|
||||
await svc_event.emit(key, data, meta);
|
||||
});
|
||||
|
||||
|
||||
this.state.extension.kv = (() => {
|
||||
const impls = this.services.get_implementors('puter-kvstore');
|
||||
const impl_kv = impls[0].impl;
|
||||
|
||||
|
||||
return new Proxy(impl_kv, {
|
||||
get: (target, prop) => {
|
||||
if ( typeof target[prop] !== 'function' ) {
|
||||
return target[prop];
|
||||
}
|
||||
|
||||
|
||||
return (...args) => {
|
||||
if ( typeof args[0] !== 'object' ) {
|
||||
// Luckily named parameters don't have positional
|
||||
@@ -152,7 +153,7 @@ class ExtensionService extends BaseService {
|
||||
this.state.extension.emit('preinit');
|
||||
}
|
||||
|
||||
async ['__on_boot.consolidation'] (...a) {
|
||||
async ['__on_boot.consolidation'](...a) {
|
||||
const svc_su = this.services.get('su');
|
||||
await svc_su.sudo(async () => {
|
||||
await this.state.extension.emit('init', {}, {
|
||||
@@ -160,7 +161,7 @@ class ExtensionService extends BaseService {
|
||||
});
|
||||
});
|
||||
}
|
||||
async ['__on_boot.activation'] (...a) {
|
||||
async ['__on_boot.activation'](...a) {
|
||||
const svc_su = this.services.get('su');
|
||||
await svc_su.sudo(async () => {
|
||||
await this.state.extension.emit('activate', {}, {
|
||||
@@ -168,7 +169,7 @@ class ExtensionService extends BaseService {
|
||||
});
|
||||
});
|
||||
}
|
||||
async ['__on_boot.ready'] (...a) {
|
||||
async ['__on_boot.ready'](...a) {
|
||||
const svc_su = this.services.get('su');
|
||||
await svc_su.sudo(async () => {
|
||||
await this.state.extension.emit('ready', {}, {
|
||||
@@ -177,7 +178,7 @@ class ExtensionService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
['__on_install.routes'] (_, { app }) {
|
||||
['__on_install.routes'](_, { app }) {
|
||||
if ( ! this.state ) debugger;
|
||||
for ( const thing of this.state.expressThings_ ) {
|
||||
if ( thing.type === 'endpoint' ) {
|
||||
|
||||
@@ -43,28 +43,25 @@ const config = require('../../../config.js');
|
||||
* @param {*} handler the handler for the router
|
||||
* @returns {express.Router} the router
|
||||
*/
|
||||
module.exports = function eggspress (route, settings, handler) {
|
||||
module.exports = function eggspress(route, settings, handler) {
|
||||
const router = express.Router();
|
||||
const mw = [];
|
||||
const afterMW = [];
|
||||
|
||||
|
||||
const _defaultJsonOptions = {};
|
||||
if ( settings.jsonCanBeLarge ) {
|
||||
_defaultJsonOptions.limit = '10mb';
|
||||
}
|
||||
|
||||
const shouldJson = settings.json === undefined && settings.noReallyItsJson === undefined ? true :
|
||||
!!(settings.json || settings.noReallyItsJson); // default true if unset, but allow explicit false
|
||||
|
||||
// These flags enable specific middleware.
|
||||
if ( settings.abuse ) mw.push(require('../../../middleware/abuse')(settings.abuse));
|
||||
if ( settings.verified ) mw.push(require('../../../middleware/verified'));
|
||||
if ( settings.json ) mw.push(express.json(_defaultJsonOptions));
|
||||
|
||||
// A hack so plain text is parsed as JSON in methods which need to be lower latency/avoid the cors roundtrip
|
||||
if ( settings.noReallyItsJson ) mw.push(express.json({ ..._defaultJsonOptions, type: '*/*' }));
|
||||
|
||||
mw.push(express.json({
|
||||
..._defaultJsonOptions,
|
||||
type: (req) => req.headers['content-type'] === "text/plain;actually=json",
|
||||
}));
|
||||
if ( shouldJson ){
|
||||
mw.push(express.json({ ..._defaultJsonOptions, type: settings.json ? undefined : settings.noReallyItsJson ? '*/*' : (req) => req.headers['content-type'] === 'text/plain;actually=json' }));
|
||||
};
|
||||
|
||||
if ( settings.auth ) mw.push(require('../../../middleware/auth'));
|
||||
if ( settings.auth2 ) mw.push(require('../../../middleware/auth2'));
|
||||
@@ -97,8 +94,8 @@ module.exports = function eggspress (route, settings, handler) {
|
||||
} catch (e) {
|
||||
return res.status(400).send({
|
||||
error: {
|
||||
message: `Invalid JSON in multipart field ${key}`
|
||||
}
|
||||
message: `Invalid JSON in multipart field ${key}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
next();
|
||||
@@ -179,9 +176,11 @@ module.exports = function eggspress (route, settings, handler) {
|
||||
});
|
||||
}
|
||||
|
||||
if ( settings.mw ) mw.push(...settings.mw);
|
||||
if ( settings.mw ){
|
||||
mw.push(...settings.mw);
|
||||
}
|
||||
|
||||
const errorHandledHandler = async function (req, res, next) {
|
||||
const errorHandledHandler = async function(req, res, next) {
|
||||
if ( settings.subdomain ) {
|
||||
if ( subdomain(req) !== settings.subdomain ) {
|
||||
return next();
|
||||
@@ -201,7 +200,7 @@ module.exports = function eggspress (route, settings, handler) {
|
||||
} else await handler(req, res, next);
|
||||
} catch (e) {
|
||||
if ( config.env === 'dev' ) {
|
||||
if (! (e instanceof APIError)) {
|
||||
if ( ! (e instanceof APIError) ) {
|
||||
// Any non-APIError indicates an unhandled error (i.e. a bug) from the backend.
|
||||
// We add a dedicated branch to facilitate debugging.
|
||||
console.error(e);
|
||||
@@ -210,57 +209,57 @@ module.exports = function eggspress (route, settings, handler) {
|
||||
api_error_handler(e, req, res, next);
|
||||
}
|
||||
};
|
||||
if (settings.allowedMethods.includes('GET')) {
|
||||
if ( settings.allowedMethods.includes('GET') ) {
|
||||
router.get(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('HEAD')) {
|
||||
if ( settings.allowedMethods.includes('HEAD') ) {
|
||||
router.head(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('POST')) {
|
||||
if ( settings.allowedMethods.includes('POST') ) {
|
||||
router.post(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('PUT')) {
|
||||
if ( settings.allowedMethods.includes('PUT') ) {
|
||||
router.put(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('DELETE')) {
|
||||
if ( settings.allowedMethods.includes('DELETE') ) {
|
||||
router.delete(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('PROPFIND')) {
|
||||
if ( settings.allowedMethods.includes('PROPFIND') ) {
|
||||
router.propfind(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('PROPPATCH')) {
|
||||
if ( settings.allowedMethods.includes('PROPPATCH') ) {
|
||||
router.proppatch(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('MKCOL')) {
|
||||
if ( settings.allowedMethods.includes('MKCOL') ) {
|
||||
router.mkcol(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('COPY')) {
|
||||
if ( settings.allowedMethods.includes('COPY') ) {
|
||||
router.copy(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('MOVE')) {
|
||||
if ( settings.allowedMethods.includes('MOVE') ) {
|
||||
router.move(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('LOCK')) {
|
||||
if ( settings.allowedMethods.includes('LOCK') ) {
|
||||
router.lock(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('UNLOCK')) {
|
||||
if ( settings.allowedMethods.includes('UNLOCK') ) {
|
||||
router.unlock(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
if (settings.allowedMethods.includes('OPTIONS')) {
|
||||
|
||||
if ( settings.allowedMethods.includes('OPTIONS') ) {
|
||||
router.options(route, ...mw, errorHandledHandler, ...afterMW);
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
};
|
||||
@@ -272,10 +272,10 @@ module.exports = eggspress(['/signup'], {
|
||||
username, email, clean_email, password, uuid, referrer,
|
||||
email_confirm_code, email_confirm_token, free_storage,
|
||||
referred_by, audit_metadata, signup_ip, signup_ip_forwarded,
|
||||
signup_user_agent, signup_origin, signup_server
|
||||
signup_user_agent, signup_origin, signup_server, metadata
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
// username
|
||||
req.body.username,
|
||||
@@ -309,6 +309,8 @@ module.exports = eggspress(['/signup'], {
|
||||
req.headers['origin'] ?? null,
|
||||
// signup_server
|
||||
config.server_id ?? null,
|
||||
// metadata
|
||||
{}
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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,13 +135,19 @@ class GetUserService extends BaseService {
|
||||
kv.set(`users:${prop}:${user[prop]}`, user);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch ( e ) {
|
||||
console.error(e);
|
||||
}
|
||||
if ( user.metadata && typeof user.metadata === 'string' ){
|
||||
user.metadata = JSON.parse(user.metadata);
|
||||
} else if ( !user.metadata ){
|
||||
user.metadata = {};
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
register_id_property (prop) {
|
||||
|
||||
register_id_property(prop) {
|
||||
this.id_properties.add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { SUB_POLICIES } from './MeteringService/subPolicies';
|
||||
|
||||
export interface IUser { uuid: string,
|
||||
username: string,
|
||||
email: string,
|
||||
subscription?: (typeof SUB_POLICIES)[number]['id'],
|
||||
metadata?: Record<string, unknown> & { hasDevAccountAccess?: boolean }
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type { BaseService } from './BaseService';
|
||||
import type { IUser } from './User';
|
||||
|
||||
export interface IInsertResult {
|
||||
insertId: number;
|
||||
}
|
||||
|
||||
export class UserService extends BaseService {
|
||||
get_system_dir(): unknown;
|
||||
generate_default_fsentries(args: { user: IUser }): Promise<void>;
|
||||
updateUserMetadata(user: IUser, updatedMetadata: Record<string, unknown>): Promise<void>;
|
||||
}
|
||||
@@ -1,51 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <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' }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,15 +50,15 @@ class UserService extends BaseService {
|
||||
this.services.emit('user.system-user-ready');
|
||||
}
|
||||
|
||||
get_system_dir () {
|
||||
get_system_dir() {
|
||||
return this.dir_system;
|
||||
}
|
||||
|
||||
// used to be called: generate_system_fsentries
|
||||
async generate_default_fsentries ({ user }) {
|
||||
|
||||
async generate_default_fsentries({ user }) {
|
||||
|
||||
this.log.noticeme('YES THIS WAS USED');
|
||||
|
||||
|
||||
// Note: The comment below is outdated as we now do parallel writes for
|
||||
// all filesystem operations. However, there may still be some
|
||||
// performance hit so this requires further investigation.
|
||||
@@ -74,7 +70,7 @@ class UserService extends BaseService {
|
||||
// by combining as many queries as we can into one and avoiding multiple back-and-forth
|
||||
// with the DB server, we can speed this process up significantly.
|
||||
|
||||
const ts = Date.now()/1000;
|
||||
const ts = Date.now() / 1000;
|
||||
|
||||
// Generate UUIDs for all the default folders and files
|
||||
const uuidv4 = this.modules.uuidv4;
|
||||
@@ -88,8 +84,7 @@ class UserService extends BaseService {
|
||||
let videos_uuid = uuidv4();
|
||||
let public_uuid = uuidv4();
|
||||
|
||||
const insert_res = await this.db.write(
|
||||
`INSERT INTO fsentries
|
||||
const insert_res = await this.db.write(`INSERT INTO fsentries
|
||||
(uuid, parent_uid, user_id, name, path, is_dir, created, modified, immutable) VALUES
|
||||
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
||||
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
||||
@@ -100,25 +95,24 @@ class UserService extends BaseService {
|
||||
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
||||
( ?, ?, ?, ?, ?, true, ?, ?, true)
|
||||
`,
|
||||
[
|
||||
// Home
|
||||
home_uuid, null, user.id, user.username, `/${user.username}`, ts, ts,
|
||||
// Trash
|
||||
trash_uuid, home_uuid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts,
|
||||
// AppData
|
||||
appdata_uuid, home_uuid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts,
|
||||
// Desktop
|
||||
desktop_uuid, home_uuid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts,
|
||||
// Documents
|
||||
documents_uuid, home_uuid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts,
|
||||
// Pictures
|
||||
pictures_uuid, home_uuid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts,
|
||||
// Videos
|
||||
videos_uuid, home_uuid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts,
|
||||
// Public
|
||||
public_uuid, home_uuid, user.id, 'Public', `/${user.username}/Public`, ts, ts,
|
||||
]
|
||||
);
|
||||
[
|
||||
// Home
|
||||
home_uuid, null, user.id, user.username, `/${user.username}`, ts, ts,
|
||||
// Trash
|
||||
trash_uuid, home_uuid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts,
|
||||
// AppData
|
||||
appdata_uuid, home_uuid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts,
|
||||
// Desktop
|
||||
desktop_uuid, home_uuid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts,
|
||||
// Documents
|
||||
documents_uuid, home_uuid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts,
|
||||
// Pictures
|
||||
pictures_uuid, home_uuid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts,
|
||||
// Videos
|
||||
videos_uuid, home_uuid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts,
|
||||
// Public
|
||||
public_uuid, home_uuid, user.id, 'Public', `/${user.username}/Public`, ts, ts,
|
||||
]);
|
||||
|
||||
// https://stackoverflow.com/a/50103616
|
||||
let trash_id = insert_res.insertId;
|
||||
@@ -135,20 +129,29 @@ class UserService extends BaseService {
|
||||
|
||||
// TODO: pass to IIAFE manager to avoid unhandled promise rejection
|
||||
// (IIAFE manager doesn't exist yet, hence this is a TODO)
|
||||
this.db.write(
|
||||
`UPDATE user SET
|
||||
this.db.write(`UPDATE user SET
|
||||
trash_uuid=?, appdata_uuid=?, desktop_uuid=?, documents_uuid=?, pictures_uuid=?, videos_uuid=?, public_uuid=?,
|
||||
trash_id=?, appdata_id=?, desktop_id=?, documents_id=?, pictures_id=?, videos_id=?, public_id=?
|
||||
|
||||
WHERE id=?`,
|
||||
[
|
||||
trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid, public_uuid,
|
||||
trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id, public_id,
|
||||
user.id
|
||||
]
|
||||
);
|
||||
[
|
||||
trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid, public_uuid,
|
||||
trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id, public_id,
|
||||
user.id,
|
||||
]);
|
||||
invalidate_cached_user(user);
|
||||
}
|
||||
|
||||
async updateUserMetadata(user, updatedMetadata){
|
||||
|
||||
let metadata = user.metadata;
|
||||
if ( !Object.keys(metadata).length ){
|
||||
metadata = updatedMetadata;
|
||||
} else {
|
||||
metadata = { ...metadata, ...updatedMetadata };
|
||||
}
|
||||
|
||||
await this.db.write('UPDATE user SET metadata=? WHERE id=?', [metadata]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
import { IUser } from '../User';
|
||||
|
||||
export class SystemActorType {
|
||||
get uid(): string;
|
||||
get_related_type(type_class: any): SystemActorType;
|
||||
get_related_type(type_class: unknown): SystemActorType;
|
||||
}
|
||||
|
||||
export class Actor {
|
||||
type: {
|
||||
app: { uid: string }
|
||||
user: { uuid: string, username: string, email: string, subscription?: (typeof SUB_POLICIES)[keyof typeof SUB_POLICIES]['id'] }
|
||||
}
|
||||
user: IUser
|
||||
};
|
||||
get uid(): string;
|
||||
clone(): Actor;
|
||||
static get_system_actor(): Actor;
|
||||
static adapt(actor?: any): Actor;
|
||||
static adapt(actor?: Actor): Actor;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BaseService } from "../BaseService";
|
||||
|
||||
export type DBMode = "DB_WRITE" | "DB_READ";
|
||||
|
||||
export interface IBaseDatabaseAccessService {
|
||||
get(): this;
|
||||
read(query: string, params?: any[]): Promise<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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `user` ADD COLUMN `metadata` TEXT DEFAULT '{}';
|
||||
@@ -16,34 +16,33 @@
|
||||
* 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,
|
||||
|
||||
+348
-217
@@ -31,45 +31,53 @@
|
||||
<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;
|
||||
}
|
||||
@@ -92,245 +100,368 @@
|
||||
<!-- 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="">
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
</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 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
</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 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 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 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 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 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 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>
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
|
||||
<!---------------------------------------->
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
|
||||
+794
-739
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
url: http://puter.localhost:4100/
|
||||
username: admin
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0IjoicyIsInYiOiIwLjAuMCIsInUiOiI3Vjlkazlxd1N0R1NjLzdFakYraXN3PT0iLCJ1dSI6ImtNa0NYbTBIU01DS2w5K2lNSE10dXc9PSIsImlhdCI6MTc1Nzc5MTcyOX0.s82vH4IKVQUIFTKW7iHzSOlTlDkqmvEzHj4fPpYDi1w
|
||||
mountpoints:
|
||||
- path: /
|
||||
provider: puterfs
|
||||
- path: /admin/tmp
|
||||
provider: memoryfs
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ES2024",
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"rootDir": ".",
|
||||
@@ -8,7 +8,7 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
@@ -17,5 +17,6 @@
|
||||
"**/tests/**",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"volatile"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user