feat: move snippets backend and tables into a plugin (B2)

This commit is contained in:
LukeGus committed 2026-09-23 08:58:53 -05:00
1 parent fdedca39fd
commit e07221eaf3
51 files changed
+28426 -3553

No files matched your search

@@ -38,10 +38,6 @@ describe("RbacAccessRepository", () => {
INSERT INTO ssh_data (
id, user_id, name, ip, port, username, credential_id, rdp_credential_id, vnc_credential_id, telnet_credential_id, folder, tags, auth_type)
VALUES (42, 'owner-1', 'prod', '10.0.0.42', 22, 'root', 123, 124, 125, 126, 'servers', 'linux', 'password');
INSERT INTO snippets (id, user_id, name, content)
VALUES
(99, 'owner-1', 'deploy', 'echo deploy'),
(100, 'owner-1', 'rollback', 'echo rollback');
INSERT INTO host_access (
id, host_id, user_id, role_id, granted_by, permission_level, expires_at, created_at
)
@@ -49,12 +45,6 @@ describe("RbacAccessRepository", () => {
(1, 42, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'),
(2, 42, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z'),
(5, 44, 'user-1', NULL, 'admin', 'view', '2026-06-25T00:00:00.000Z', '2026-06-24T00:00:00.000Z');
INSERT INTO snippet_access (
id, snippet_id, user_id, role_id, granted_by, permission_level, expires_at, created_at
)
VALUES
(3, 99, 'user-1', NULL, 'admin', 'view', NULL, '2026-06-26T00:00:00.000Z'),
(4, 99, NULL, 7, 'admin', 'view', '2026-06-27T00:00:00.000Z', '2026-06-26T01:00:00.000Z');
INSERT INTO shared_host_secrets (
id, host_access_id, target_user_id, protocol, source_type, original_credential_id, encrypted_username, encrypted_auth_type
)
@@ -95,29 +85,6 @@ describe("RbacAccessRepository", () => {
]);
});
it("lists snippet access with user and role target metadata", async () => {
const repo = await createRepository();
const accessList = await repo.listSnippetAccess(99);
expect(accessList.map((access) => access.targetType)).toEqual([
"role",
"user",
]);
expect(accessList[0]).toMatchObject({
id: 4,
roleId: 7,
roleName: "ops",
grantedByUsername: "admin",
});
expect(accessList[1]).toMatchObject({
id: 3,
userId: "user-1",
username: "alice",
grantedByUsername: "admin",
});
});
it("lists shared hosts for direct and role access", async () => {
const repo = await createRepository();
@@ -253,67 +220,6 @@ describe("RbacAccessRepository", () => {
expect(writeCount).toBe(1);
});
it("lists shared snippets and preserves route-level direct-over-role behavior", async () => {
const repo = await createRepository();
const sharedSnippets = await repo.listSharedSnippets(
"user-1",
[7],
activeAccessTime,
);
expect(sharedSnippets).toHaveLength(1);
expect(sharedSnippets[0]).toMatchObject({
id: 99,
name: "deploy",
ownerUsername: "owner",
permissionLevel: "view",
});
});
it("lists visible shared snippets for the main snippets route", async () => {
const repo = await createRepository();
const sharedSnippets = await repo.listVisibleSharedSnippets(
"user-1",
[7],
activeAccessTime,
);
expect(sharedSnippets.map((snippet) => snippet.id)).toEqual([99, 99]);
expect(sharedSnippets[0]).toMatchObject({
userId: "owner-1",
name: "deploy",
content: "echo deploy",
ownerUsername: "owner",
});
});
it("finds an accessible shared snippet for direct or role access", async () => {
const repo = await createRepository();
await expect(
repo.findAccessibleSharedSnippet(99, "user-2", [7], activeAccessTime),
).resolves.toMatchObject({
id: 99,
userId: "owner-1",
name: "deploy",
content: "echo deploy",
ownerUsername: "owner",
permissionLevel: "view",
hostFilter: null,
});
await expect(
repo.findAccessibleSharedSnippet(
99,
"user-2",
[7],
"2026-06-28T00:00:00.000Z",
),
).resolves.toBeNull();
});
it("upserts host access and updates overrides", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
@@ -432,39 +338,4 @@ describe("RbacAccessRepository", () => {
expect(await repo.deleteHostAccessForUserReferences("admin")).toBe(0);
expect(writeCount).toBe(1);
});
it("upserts and revokes snippet access", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
const updated = await repo.upsertSnippetAccess({
snippetId: 99,
targetType: "user",
targetUserId: "user-1",
grantedBy: "admin",
expiresAt: "2026-06-28T00:00:00.000Z",
});
expect(updated).toEqual({ id: 3, created: false });
expect(
(await repo.listSnippetAccess(99)).find((row) => row.id === 3),
).toMatchObject({ expiresAt: "2026-06-28T00:00:00.000Z" });
const created = await repo.upsertSnippetAccess({
snippetId: 100,
targetType: "role",
targetRoleId: 7,
grantedBy: "admin",
expiresAt: null,
});
expect(created.created).toBe(true);
await repo.revokeSnippetAccess(3, 99);
expect((await repo.listSnippetAccess(99)).map((row) => row.id)).toEqual([
4,
]);
expect(writeCount).toBe(3);
});
});
@@ -1,356 +0,0 @@
import { sql } from "drizzle-orm";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { SnippetRepository } from "../../../database/repositories/snippet-repository.js";
describe("SnippetRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(onWrite?: () => void): Promise<{
repository: SnippetRepository;
}> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash) VALUES
('user-1', 'user-1', 'hash'),
('user-2', 'user-2', 'hash');
INSERT INTO snippets (
id, user_id, name, content, description, folder, "order", host_filter
)
VALUES
(1, 'user-1', 'root', 'uptime', NULL, NULL, 2, NULL),
(2, 'user-1', 'deploy', 'make deploy', 'Deploy app', 'ops', 1, 'linux'),
(3, 'user-2', 'other', 'whoami', NULL, NULL, 1, NULL);
INSERT INTO snippet_folders (id, user_id, name, color, icon)
VALUES
(1, 'user-1', 'ops', '#123456', 'terminal'),
(2, 'user-1', 'db', NULL, NULL),
(3, 'user-2', 'other', NULL, NULL);
`);
return {
repository: new SnippetRepository(context, onWrite),
};
}
it("finds owned snippets only", async () => {
const { repository } = await createRepository();
await expect(repository.findOwnedById("user-1", 1)).resolves.toMatchObject({
id: 1,
userId: "user-1",
name: "root",
});
await expect(repository.findOwnedById("user-1", 3)).resolves.toBeNull();
await expect(repository.findOwnedById("user-1", 999)).resolves.toBeNull();
});
it("lists folders by name for a user", async () => {
const { repository } = await createRepository();
const rows = await repository.listFolders("user-1");
expect(rows.map((row) => row.name)).toEqual(["db", "ops"]);
});
it("lists export data for a user", async () => {
const { repository } = await createRepository();
const snippets = await repository.listSnippetsForExport("user-1");
const folders = await repository.listFoldersForExport("user-1");
expect(snippets.map((row) => row.name)).toEqual(["root", "deploy"]);
expect(folders.map((row) => row.name)).toEqual(["db", "ops"]);
});
it("lists owned snippets for route merging", async () => {
const { repository } = await createRepository();
const rows = await repository.listOwnedSnippets("user-1");
expect(rows.map((row) => row.name)).toEqual(["root", "deploy"]);
});
it("reorders snippets", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
await repository.reorderSnippets("user-1", [
{ id: 1, order: 9, folder: " ops " },
{ id: 999, order: 1 },
]);
await expect(repository.findOwnedById("user-1", 1)).resolves.toMatchObject({
order: 9,
folder: "ops",
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("creates snippets with the next folder order", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const created = await repository.createSnippet("user-1", {
name: " new ",
content: " echo ok ",
description: " desc ",
folder: "ops",
hostFilter: { os: "linux" },
});
expect(created).toMatchObject({
userId: "user-1",
name: "new",
content: "echo ok",
description: "desc",
folder: "ops",
order: 2,
hostFilter: JSON.stringify({ os: "linux" }),
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("updates snippets and returns the original row for audit names", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const result = await repository.updateSnippet("user-1", 2, {
name: " deploy new ",
content: " make deploy2 ",
description: null,
folder: null,
order: 7,
hostFilter: null,
});
const missing = await repository.updateSnippet("user-1", 3, {
name: "nope",
});
expect(result?.existing).toMatchObject({ name: "deploy" });
expect(result?.updated).toMatchObject({
name: "deploy new",
content: "make deploy2",
description: null,
folder: null,
order: 7,
hostFilter: null,
});
expect(missing).toBeNull();
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("deletes snippets and returns the deleted row for audit names", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const deleted = await repository.deleteSnippet("user-1", 2);
const missing = await repository.deleteSnippet("user-1", 3);
expect(deleted).toMatchObject({ id: 2, name: "deploy" });
expect(missing).toBeNull();
await expect(repository.findOwnedById("user-1", 2)).resolves.toBeNull();
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("deletes all snippets and folders for a user", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
await expect(repository.deleteByUserId("user-1")).resolves.toEqual({
snippetsDeleted: 2,
foldersDeleted: 2,
});
expect(
await adapter!.query(sql`SELECT id FROM snippets ORDER BY id`),
).toEqual([{ id: 3 }]);
expect(
await adapter!.query(sql`SELECT id FROM snippet_folders ORDER BY id`),
).toEqual([{ id: 3 }]);
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("bulk imports folders and snippets", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const result = await repository.bulkImport(
"user-1",
[
{
name: " new snippet ",
content: " echo hi ",
description: " desc ",
folder: " new folder ",
hostFilter: "linux",
},
{ name: "", content: "bad" },
{ name: "deploy", content: "skip", folder: "ops" },
],
[
{ name: " new folder ", color: " #fff ", icon: " star " },
{ name: "ops" },
{ name: "" },
],
false,
);
expect(result).toEqual({
snippetsImported: 1,
snippetsSkipped: 1,
snippetsUpdated: 0,
foldersImported: 1,
foldersSkipped: 1,
failed: 2,
errors: [
"Folder missing name",
"Snippet 2: name and content are required",
],
});
await expect(repository.findOwnedById("user-1", 4)).resolves.toMatchObject({
name: "new snippet",
content: "echo hi",
description: "desc",
folder: "new folder",
order: 0,
hostFilter: "linux",
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("bulk import overwrites existing snippets", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const result = await repository.bulkImport(
"user-1",
[
{
name: "deploy",
content: "make deploy v2",
folder: "ops",
order: 5,
hostFilter: "prod",
},
],
undefined,
true,
);
expect(result).toMatchObject({
snippetsImported: 0,
snippetsSkipped: 0,
snippetsUpdated: 1,
failed: 0,
});
await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
content: "make deploy v2",
order: 5,
hostFilter: "prod",
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("creates folders and rejects duplicate names", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const created = await repository.createFolder(
"user-1",
" new ",
" #fff ",
" star ",
);
const duplicate = await repository.createFolder(
"user-1",
"ops",
null,
null,
);
expect(created).toMatchObject({
userId: "user-1",
name: "new",
color: "#fff",
icon: "star",
});
expect(duplicate).toBeNull();
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("updates folder metadata", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
const updated = await repository.updateFolderMetadata(
"user-1",
"ops",
" #abc ",
undefined,
);
const missing = await repository.updateFolderMetadata(
"user-1",
"missing",
null,
null,
);
expect(updated).toMatchObject({
name: "ops",
color: "#abc",
icon: "terminal",
});
expect(missing).toBeNull();
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("renames folders and attached snippets", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
await expect(
repository.renameFolder("user-1", "missing", "new"),
).resolves.toEqual({ status: "missing" });
await expect(
repository.renameFolder("user-1", "ops", "db"),
).resolves.toEqual({
status: "conflict",
});
await expect(
repository.renameFolder("user-1", "ops", "deploys"),
).resolves.toEqual({ status: "renamed" });
await expect(repository.listFolders("user-1")).resolves.toEqual(
expect.arrayContaining([expect.objectContaining({ name: "deploys" })]),
);
await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
folder: "deploys",
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
it("deletes folders and moves snippets to the root", async () => {
const onWrite = vi.fn();
const { repository } = await createRepository(onWrite);
await repository.deleteFolder("user-1", "ops");
expect(
(await repository.listFolders("user-1")).map((row) => row.name),
).toEqual(["db"]);
await expect(repository.findOwnedById("user-1", 2)).resolves.toMatchObject({
folder: null,
});
expect(onWrite).toHaveBeenCalledTimes(1);
});
});
@@ -1,79 +0,0 @@
import { describe, expect, it } from "vitest";
import {
createSnippetExecutionResult,
getSnippetExecutionTimeoutMs,
resolveSnippetCommand,
} from "../../../database/routes/snippets-execution.js";
describe("snippet execution", () => {
it("treats stderr as diagnostic output when the command succeeds", () => {
expect(createSnippetExecutionResult(0, "done\n", "warning\n")).toEqual({
success: true,
output: "done\n",
error: "warning\n",
});
});
it("uses the exit code to report command failure", () => {
expect(createSnippetExecutionResult(1, "", "failed\n")).toEqual({
success: false,
output: "",
error: "failed\n",
});
});
it("preserves the previous fallback when no exit code is available", () => {
expect(createSnippetExecutionResult(null, "done\n", "")).toEqual({
success: true,
output: "done\n",
});
expect(createSnippetExecutionResult(null, "", "failed\n").success).toBe(
false,
);
});
it("disables the command timeout by default", () => {
expect(getSnippetExecutionTimeoutMs(undefined)).toBeUndefined();
});
it("converts a configured timeout from seconds to milliseconds", () => {
expect(getSnippetExecutionTimeoutMs("45")).toBe(45_000);
});
it.each(["", "0", "-1", "invalid"])(
"ignores invalid timeout value %j",
(value) => {
expect(getSnippetExecutionTimeoutMs(value)).toBeUndefined();
},
);
});
describe("resolveSnippetCommand", () => {
const host = { ip: "10.0.0.5", username: "root", port: 22, name: "web-01" };
it("substitutes host variables per target host", () => {
expect(
resolveSnippetCommand("ssh $USER@$HOST -p $PORT # $NAME", host),
).toBe("ssh root@10.0.0.5 -p 22 # web-01");
});
it("supports brace syntax for host variables", () => {
expect(resolveSnippetCommand("ping ${HOST}", host)).toBe("ping 10.0.0.5");
});
it("substitutes input placeholders from inputValues", () => {
expect(
resolveSnippetCommand("nc -zv $HOST ${INPUT_1:Port}", host, {
INPUT_1: "8080",
}),
).toBe("nc -zv 10.0.0.5 8080");
});
it("leaves host variables literal when no host context is given", () => {
expect(resolveSnippetCommand("ping $HOST", null)).toBe("ping $HOST");
});
it("leaves input placeholders literal when no value was supplied", () => {
expect(resolveSnippetCommand("echo $INPUT_1", null)).toBe("echo $INPUT_1");
});
});
@@ -1,20 +0,0 @@
import fs from "fs";
import path from "path";
import { describe, expect, it } from "vitest";
describe("snippet execution host access", () => {
it("uses the shared-host-aware resolver", () => {
const source = fs.readFileSync(
path.resolve(__dirname, "../../../database/routes/snippets.ts"),
"utf8",
);
const executeRoute = source.slice(
source.indexOf('router.post(\n "/execute"'),
);
expect(executeRoute).toContain(
"const host = await resolveHostById(parseInt(hostId), userId)",
);
expect(executeRoute).not.toContain("host.userId !== userId");
});
});
@@ -33,8 +33,6 @@ describe("isValidEntityType", () => {
"hosts",
"sshCredentials",
"sshFolders",
"snippets",
"snippetFolders",
"vaultProfiles",
"dashboardServiceLinks",
"homepageItems",
@@ -88,7 +86,7 @@ describe("stripWritePayload", () => {
it("does not mutate the original payload object", () => {
const payload = { id: 1, userId: "user-1", syncId: "abc", name: "x" };
stripWritePayload("snippets", payload);
stripWritePayload("sshFolders", payload);
expect(payload).toEqual({
id: 1,
userId: "user-1",
@@ -6,6 +6,7 @@ const calls = vi.hoisted(() => ({
deletedFor: [] as string[],
rotatedFor: [] as string[],
legacyWrapsDeletedFor: [] as string[],
dataWipedEvents: [] as Array<{ userId: string }>,
}));
function deletingRepo(label: string) {
@@ -29,11 +30,19 @@ vi.mock("../../../database/repositories/factory.js", () => ({
createCurrentFileManagerBookmarkRepository: deletingRepo("bookmarks"),
createCurrentRecentActivityRepository: deletingRepo("activity"),
createCurrentDismissedAlertRepository: deletingRepo("alerts"),
createCurrentSnippetRepository: deletingRepo("snippets"),
createCurrentHostRepository: deletingRepo("hosts"),
createCurrentCredentialRepository: deletingRepo("credentials"),
}));
vi.mock("../../../plugins/events.js", () => ({
pluginEvents: {
emit: (_topic: string, payload: { userId: string }) => {
calls.dataWipedEvents.push(payload);
},
},
TOPICS: { userDataWiped: "user.data_wiped" },
}));
vi.mock("../../../utils/user-keys.js", () => ({
UserKeyManager: {
getInstance: () => ({
@@ -65,6 +74,7 @@ beforeEach(() => {
calls.deletedFor = [];
calls.rotatedFor = [];
calls.legacyWrapsDeletedFor = [];
calls.dataWipedEvents = [];
});
describe("resetUserPassword", () => {
@@ -110,10 +120,10 @@ describe("resetUserPassword", () => {
"bookmarks:user-1",
"activity:user-1",
"alerts:user-1",
"snippets:user-1",
"hosts:user-1",
"credentials:user-1",
]);
expect(calls.dataWipedEvents).toEqual([{ userId: "user-1" }]);
expect(calls.rotatedFor).toEqual(["user-1"]);
expect(calls.legacyWrapsDeletedFor).toEqual(["user-1"]);
expect(