mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 17:50:00 -06:00
dev: add threads API with create and edit
This commit is contained in:
@@ -364,6 +364,9 @@ const install = async ({ services, app, useapi, modapi }) => {
|
||||
|
||||
const { RequestMeasureService } = require('./services/RequestMeasureService');
|
||||
services.registerService('request-measure', RequestMeasureService);
|
||||
|
||||
const { ThreadService } = require('./services/ThreadService');
|
||||
services.registerService('thread', ThreadService);
|
||||
}
|
||||
|
||||
const install_legacy = async ({ services }) => {
|
||||
|
||||
@@ -479,6 +479,11 @@ module.exports = class APIError {
|
||||
status: 400,
|
||||
message: 'Incorrect or missing anti-CSRF token.',
|
||||
},
|
||||
|
||||
'not_yet_supported': {
|
||||
status: 400,
|
||||
message: ({ message }) => message,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
301
src/backend/src/services/ThreadService.js
Normal file
301
src/backend/src/services/ThreadService.js
Normal file
@@ -0,0 +1,301 @@
|
||||
const { PermissionImplicator, PermissionUtil } = require("./auth/PermissionService");
|
||||
const BaseService = require("./BaseService")
|
||||
|
||||
const APIError = require("../api/APIError");
|
||||
const { is_valid_uuid } = require("../helpers");
|
||||
const { Context } = require("../util/context");
|
||||
const { DB_WRITE } = require("./database/consts");
|
||||
const configurable_auth = require("../middleware/configurable_auth");
|
||||
const { Endpoint } = require("../util/expressutil");
|
||||
const { whatis } = require("../util/langutil");
|
||||
const { UserActorType } = require("./auth/Actor");
|
||||
|
||||
class ThreadService extends BaseService {
|
||||
static MODULES = {
|
||||
uuidv4: require('uuid').v4,
|
||||
};
|
||||
|
||||
async _init () {
|
||||
this.db = this.services.get('database').get(DB_WRITE, 'service:thread');
|
||||
|
||||
this.thread_body_max_size = 4 * 1024; // 4KiB
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
svc_permission.register_implicator(PermissionImplicator.create({
|
||||
id: 'is-thread-owner',
|
||||
matcher: permission => {
|
||||
return permission.startsWith('thread:');
|
||||
},
|
||||
checker: async ({ actor, permission }) => {
|
||||
if ( !(actor.type instanceof UserActorType) ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [_, uid] = PermissionUtil.split(permission);
|
||||
|
||||
const thread = await this.get_thread({ uid });
|
||||
if (
|
||||
thread.owner_user_id === actor.type.user.id &&
|
||||
thread.parent_uid === null
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
const NO_RECURSE_PERMISSIONS = ['children-of', 'own-children-of'];
|
||||
svc_permission.register_implicator(PermissionImplicator.create({
|
||||
id: 'children-of',
|
||||
matcher: permission => {
|
||||
if ( ! permission.startsWith('thread:') ) return;
|
||||
const [_, uid, ...rest] = PermissionUtil.split(permission);
|
||||
if ( rest.length > 0 && NO_RECURSE_PERMISSIONS.includes(rest[0]) ) {
|
||||
return undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
checker: async ({ actor, permission }) => {
|
||||
const [_, uid, ...rest] = PermissionUtil.split(permission);
|
||||
|
||||
const thread = await this.get_thread({ uid });
|
||||
const parent_uid = thread.parent_uid;
|
||||
if ( parent_uid === null ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
const reading = await svc_permission.scan(
|
||||
actor,
|
||||
PermissionUtil.join('thread', parent_uid, 'children-of', ...rest),
|
||||
);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length <= 0 ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}));
|
||||
svc_permission.register_implicator(PermissionImplicator.create({
|
||||
id: 'own-children-of',
|
||||
matcher: permission => {
|
||||
if ( ! permission.startsWith('thread:') ) return;
|
||||
const [_, uid, ...rest] = PermissionUtil.split(permission);
|
||||
debugger;
|
||||
if ( rest.length > 0 && NO_RECURSE_PERMISSIONS.includes(rest[0]) ) {
|
||||
return undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
checker: async ({ actor, permission }) => {
|
||||
const [_, uid, ...rest] = PermissionUtil.split(permission);
|
||||
|
||||
const thread = await this.get_thread({ uid });
|
||||
const parent_uid = thread.parent_uid;
|
||||
if ( parent_uid === null ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log('own children implicator', {
|
||||
permission
|
||||
});
|
||||
|
||||
if ( thread.owner_user_id !== actor.type.user.id ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
const reading = await svc_permission.scan(
|
||||
actor,
|
||||
PermissionUtil.join('thread', parent_uid, 'own-children-of', ...rest),
|
||||
);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length <= 0 ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}));
|
||||
}
|
||||
async ['__on_install.routes'] (_, { app }) {
|
||||
const r_threads = (() => {
|
||||
const require = this.require;
|
||||
const express = require('express');
|
||||
return express.Router();
|
||||
})();
|
||||
app.use('/threads', r_threads);
|
||||
this.install_threads_endpoints_({ router: r_threads });
|
||||
}
|
||||
|
||||
install_threads_endpoints_ ({ router }) {
|
||||
Endpoint({
|
||||
route: '/create',
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
const text = req.body.text;
|
||||
|
||||
if ( whatis(text) !== 'string' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'text',
|
||||
expected: 'string',
|
||||
got: whatis(text),
|
||||
});
|
||||
}
|
||||
if ( text.length > this.thread_body_max_size ) {
|
||||
throw APIError.create('field_too_large', null, {
|
||||
key: 'text',
|
||||
max_size: this.thread_body_max_size,
|
||||
size: text.length,
|
||||
});
|
||||
}
|
||||
|
||||
const uid = this.modules.uuidv4();
|
||||
|
||||
const parent_uid = req.body.parent ?? null;
|
||||
if ( parent_uid !== null ) {
|
||||
if ( whatis(parent_uid) !== 'string' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'parent',
|
||||
expected: 'string',
|
||||
got: whatis(parent_uid),
|
||||
});
|
||||
}
|
||||
|
||||
// Disable deep-nesting for now
|
||||
{
|
||||
const parent_thread = await this.get_thread({ uid: parent_uid });
|
||||
if ( !parent_thread ) {
|
||||
throw APIError.create('thread_not_found', null, {
|
||||
uid: parent_uid,
|
||||
});
|
||||
}
|
||||
|
||||
if ( parent_thread.parent_uid ) {
|
||||
throw APIError.create('not_yet_supported', null, {
|
||||
message: 'deeply nested threads are not yet supported',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
const reading = await svc_permission.scan(
|
||||
actor,
|
||||
PermissionUtil.join('thread', parent_uid, 'post'),
|
||||
);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length <= 0 ) {
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission: 'thread:' + parent_uid + ':post',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( parent_uid === null ) {
|
||||
console.log('its this one');
|
||||
await this.db.write(
|
||||
"INSERT INTO `thread` (uid, owner_user_id, text) VALUES (?, ?, ?)",
|
||||
[uid, actor.type.user.id, text]
|
||||
);
|
||||
} else {
|
||||
console.log('its tHAT one');
|
||||
await this.db.write(
|
||||
"INSERT INTO `thread` (uid, parent_uid, owner_user_id, text) VALUES (?, ?, ?, ?)",
|
||||
[uid, parent_uid, actor.type.user.id, text]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ uid });
|
||||
}
|
||||
}).attach(router);
|
||||
|
||||
Endpoint({
|
||||
route: '/edit',
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const text = req.body.text;
|
||||
|
||||
if ( whatis(text) !== 'string' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'text',
|
||||
expected: 'string',
|
||||
got: whatis(text),
|
||||
});
|
||||
}
|
||||
if ( text.length > this.thread_body_max_size ) {
|
||||
throw APIError.create('field_too_large', null, {
|
||||
key: 'text',
|
||||
max_size: this.thread_body_max_size,
|
||||
size: text.length,
|
||||
});
|
||||
}
|
||||
|
||||
const uid = req.body.uid;
|
||||
|
||||
if ( ! is_valid_uuid(uid) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'uid',
|
||||
expected: 'uuid',
|
||||
got: whatis(uid),
|
||||
});
|
||||
}
|
||||
|
||||
// Get existing thread
|
||||
const thread = await this.get_thread({ uid });
|
||||
console.log('thread???', thread);
|
||||
if ( !thread ) {
|
||||
throw APIError.create('thread_not_found', null, {
|
||||
uid,
|
||||
});
|
||||
}
|
||||
|
||||
const actor = Context.get('actor');
|
||||
|
||||
// Check edit permission
|
||||
{
|
||||
const permission = PermissionUtil.join('thread', uid, 'edit');
|
||||
const svc_permission = this.services.get('permission');
|
||||
const reading = await svc_permission.scan(actor, permission);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length <= 0 ) {
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update thread
|
||||
await this.db.write(
|
||||
"UPDATE `thread` SET text=? WHERE uid=?",
|
||||
[text, uid]
|
||||
);
|
||||
|
||||
res.json({});
|
||||
}
|
||||
}).attach(router);
|
||||
}
|
||||
|
||||
async get_thread ({ uid }) {
|
||||
const [thread] = await this.db.read(
|
||||
"SELECT * FROM `thread` WHERE uid=?",
|
||||
[uid]
|
||||
);
|
||||
|
||||
if ( !thread ) {
|
||||
throw APIError.create('thread_not_found', null, {
|
||||
uid,
|
||||
});
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ThreadService,
|
||||
};
|
||||
@@ -155,6 +155,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
[31, [
|
||||
'0034_app-redirect.sql',
|
||||
]],
|
||||
[32, [
|
||||
'0035_threads.sql',
|
||||
]]
|
||||
];
|
||||
|
||||
// Database upgrade logic
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE `thread` (
|
||||
`id` INTEGER PRIMARY KEY,
|
||||
`uid` TEXT NOT NULL UNIQUE,
|
||||
`parent_uid` TEXT NULL DEFAULT NULL,
|
||||
`owner_user_id` INTEGER NOT NULL,
|
||||
`schema` TEXT NULL DEFAULT NULL,
|
||||
`text` TEXT NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY("parent_uid") REFERENCES "thread" ("uid") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY("owner_user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_thread_uid` ON `thread` (`uid`);
|
||||
Reference in New Issue
Block a user