mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
168 lines
5.5 KiB
TypeScript
168 lines
5.5 KiB
TypeScript
import type { Router } from "express";
|
|
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
|
import { snippetAccess, snippetFolders, snippets } from "./tables.js";
|
|
import { createSnippetRepository } from "./repository.js";
|
|
import { registerSnippetRoutes } from "./routes.js";
|
|
import { resolveSnippetCommand } from "./execution.js";
|
|
|
|
/** What other plugins get from ctx.services.get("snippets.access", {userId}). */
|
|
export interface SnippetsService {
|
|
/** The calling user's own snippets (not shared ones), full content included. */
|
|
list: () => Promise<
|
|
Array<{
|
|
id: number;
|
|
name: string;
|
|
content: string;
|
|
description: string | null;
|
|
isNote: boolean;
|
|
folder: string | null;
|
|
}>
|
|
>;
|
|
/** A snippet the calling user owns or was shared with them. */
|
|
get: (id: number) => Promise<{
|
|
id: number;
|
|
name: string;
|
|
content: string;
|
|
isNote: boolean;
|
|
} | null>;
|
|
resolveCommand: (
|
|
id: number,
|
|
vars: { ip?: string; username?: string; port?: number; name?: string },
|
|
inputValues?: Record<string, string>,
|
|
) => Promise<string | null>;
|
|
/** Creates a snippet owned by the calling user. */
|
|
create: (input: {
|
|
name: string;
|
|
content: string;
|
|
description?: string | null;
|
|
folder?: string | null;
|
|
}) => Promise<{ id: number; name: string }>;
|
|
/** Updates a snippet the calling user owns. Throws if not found. */
|
|
update: (
|
|
id: number,
|
|
changes: {
|
|
name?: string;
|
|
content?: string;
|
|
description?: string | null;
|
|
folder?: string | null;
|
|
},
|
|
) => Promise<void>;
|
|
/** Deletes a snippet the calling user owns. Returns whether one was deleted. */
|
|
remove: (id: number) => Promise<boolean>;
|
|
}
|
|
|
|
export async function activate(ctx: PluginContext) {
|
|
const snippetsTable = await ctx.db.define(snippets);
|
|
const foldersTable = await ctx.db.define(snippetFolders);
|
|
const accessTable = await ctx.db.define(snippetAccess);
|
|
|
|
const repo = createSnippetRepository(
|
|
ctx.db,
|
|
snippetsTable,
|
|
foldersTable,
|
|
accessTable,
|
|
);
|
|
|
|
registerSnippetRoutes(ctx.http.router<Router>(), repo, ctx);
|
|
|
|
ctx.sync.registerEntity({
|
|
type: "snippetFolders",
|
|
table: foldersTable,
|
|
order: 40,
|
|
});
|
|
ctx.sync.registerEntity({
|
|
type: "snippets",
|
|
table: snippetsTable,
|
|
order: 60,
|
|
// Host quick actions and keybindings in core point at snippets by this.
|
|
answersTo: ["commandSnippet"],
|
|
});
|
|
|
|
// The user row survives a password-reset data wipe, so it never triggers
|
|
// our refUser() cascade; this topic is core's way of telling us to drop
|
|
// our copy of that user's data too.
|
|
ctx.events.on("user.data_wiped", (payload) => {
|
|
const userId = (payload as { userId?: string } | null)?.userId;
|
|
if (userId) void repo.deleteByUserId(userId);
|
|
});
|
|
|
|
async function findAccessible(userId: string, id: number) {
|
|
const owned = await repo.findOwnedById(userId, id);
|
|
if (owned) return owned;
|
|
const roleIds = await repo.listUserRoleIds(userId);
|
|
return repo.findAccessibleSharedSnippet(id, userId, roleIds);
|
|
}
|
|
|
|
// The service is gated on snippets.view; a write also needs the same
|
|
// permission its route requires, so a caller cannot write through here
|
|
// what the user could not write in the panel.
|
|
async function requirePermission(permission: string): Promise<void> {
|
|
if (!(await ctx.rbac.has(permission))) {
|
|
throw new Error(`Missing permission snippets.${permission}`);
|
|
}
|
|
}
|
|
|
|
const service: SnippetsService = {
|
|
list: async () => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) return [];
|
|
const owned = await repo.listOwnedSnippets(userId);
|
|
return owned.map((s) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
content: s.content,
|
|
description: s.description,
|
|
isNote: s.isNote,
|
|
folder: s.folder,
|
|
}));
|
|
},
|
|
get: async (id) => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) return null;
|
|
const snippet = await findAccessible(userId, id);
|
|
if (!snippet) return null;
|
|
return {
|
|
id: snippet.id,
|
|
name: snippet.name,
|
|
content: snippet.content,
|
|
isNote: snippet.isNote,
|
|
};
|
|
},
|
|
resolveCommand: async (id, vars, inputValues = {}) => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) return null;
|
|
const snippet = await findAccessible(userId, id);
|
|
if (!snippet) return null;
|
|
return resolveSnippetCommand(snippet.content, vars, inputValues);
|
|
},
|
|
create: async (input) => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) throw new Error("snippets.access.create needs an actor");
|
|
await requirePermission("create");
|
|
const created = await repo.createSnippet(userId, input);
|
|
return { id: created.id, name: created.name };
|
|
},
|
|
update: async (id, changes) => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) throw new Error("snippets.access.update needs an actor");
|
|
await requirePermission("edit");
|
|
const result = await repo.updateSnippet(userId, id, changes);
|
|
if (!result) throw new Error("Snippet not found");
|
|
},
|
|
remove: async (id) => {
|
|
const userId = ctx.currentActor();
|
|
if (!userId) throw new Error("snippets.access.remove needs an actor");
|
|
await requirePermission("delete");
|
|
const existing = await repo.deleteSnippet(userId, id);
|
|
return !!existing;
|
|
},
|
|
};
|
|
ctx.services.provide("snippets.access", service);
|
|
|
|
ctx.log.info("Snippets routes mounted at /plugin-api/snippets");
|
|
}
|
|
|
|
export async function deactivate() {
|
|
// Everything above was registered through ctx and is disposed by core.
|
|
}
|