v1.9.0 #437

Merged
LukeGus merged 33 commits from dev-1.9.0 into main 2025-11-17 15:46:05 +00:00
6 changed files with 1506 additions and 272 deletions
Showing only changes of commit 16e28c6e6b - Show all commits

View File

@@ -386,14 +386,14 @@ const addColumnIfNotExists = (
try { try {
sqlite sqlite
.prepare( .prepare(
`SELECT ${column} `SELECT "${column}"
FROM ${table} LIMIT 1`, FROM ${table} LIMIT 1`,
) )
.get(); .get();
} catch { } catch {
try { try {
sqlite.exec(`ALTER TABLE ${table} sqlite.exec(`ALTER TABLE ${table}
ADD COLUMN ${column} ${definition};`); ADD COLUMN "${column}" ${definition};`);
} catch (alterError) { } catch (alterError) {
databaseLogger.warn(`Failed to add column ${column} to ${table}`, { databaseLogger.warn(`Failed to add column ${column} to ${table}`, {
operation: "schema_migration", operation: "schema_migration",
@@ -495,6 +495,35 @@ const migrateSchema = () => {
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL"); addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("snippets", "folder", "TEXT");
addColumnIfNotExists("snippets", "order", "INTEGER NOT NULL DEFAULT 0");
try {
sqlite
.prepare("SELECT id FROM snippet_folders LIMIT 1")
.get();
} catch {
try {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS snippet_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
color TEXT,
icon TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
`);
} catch (createError) {
databaseLogger.warn("Failed to create snippet_folders table", {
operation: "schema_migration",
error: createError,
});
}
}
try { try {
sqlite sqlite
.prepare("SELECT id FROM sessions LIMIT 1") .prepare("SELECT id FROM sessions LIMIT 1")

View File

@@ -206,6 +206,24 @@ export const snippets = sqliteTable("snippets", {
name: text("name").notNull(), name: text("name").notNull(),
content: text("content").notNull(), content: text("content").notNull(),
description: text("description"), description: text("description"),
folder: text("folder"),
order: integer("order").notNull().default(0),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
updatedAt: text("updated_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
export const snippetFolders = sqliteTable("snippet_folders", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
color: text("color"),
icon: text("icon"),
createdAt: text("created_at") createdAt: text("created_at")
.notNull() .notNull()
.default(sql`CURRENT_TIMESTAMP`), .default(sql`CURRENT_TIMESTAMP`),

View File

@@ -1,8 +1,8 @@
import type { AuthenticatedRequest } from "../../../types/index.js"; import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express"; import express from "express";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { snippets } from "../db/schema.js"; import { snippets, snippetFolders } from "../db/schema.js";
import { eq, and, desc, sql } from "drizzle-orm"; import { eq, and, desc, asc, sql } from "drizzle-orm";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { authLogger } from "../../utils/logger.js"; import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
@@ -17,241 +17,389 @@ const authManager = AuthManager.getInstance();
const authenticateJWT = authManager.createAuthMiddleware(); const authenticateJWT = authManager.createAuthMiddleware();
const requireDataAccess = authManager.createDataAccessMiddleware(); const requireDataAccess = authManager.createDataAccessMiddleware();
// Get all snippets for the authenticated user // Get all snippet folders
// GET /snippets // GET /snippets/folders
router.get( router.get(
"/", "/folders",
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) { if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippets fetch"); authLogger.warn("Invalid userId for snippet folders fetch");
return res.status(400).json({ error: "Invalid userId" }); return res.status(400).json({ error: "Invalid userId" });
} }
try { try {
const result = await db const result = await db
.select() .select()
.from(snippets) .from(snippetFolders)
.where(eq(snippets.userId, userId)) .where(eq(snippetFolders.userId, userId))
.orderBy(desc(snippets.updatedAt)); .orderBy(asc(snippetFolders.name));
res.json(result); res.json(result);
} catch (err) { } catch (err) {
authLogger.error("Failed to fetch snippets", err); authLogger.error("Failed to fetch snippet folders", err);
res.status(500).json({ error: "Failed to fetch snippets" }); res.status(500).json({ error: "Failed to fetch snippet folders" });
} }
}, },
); );
// Get a specific snippet by ID // Create a new snippet folder
// GET /snippets/:id // POST /snippets/folders
router.get(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const snippetId = parseInt(id, 10);
if (!isNonEmptyString(userId) || isNaN(snippetId)) {
authLogger.warn("Invalid request for snippet fetch: invalid ID", {
userId,
id,
});
return res.status(400).json({ error: "Invalid request parameters" });
}
try {
const result = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (result.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
res.json(result[0]);
} catch (err) {
authLogger.error("Failed to fetch snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to fetch snippet",
});
}
},
);
// Create a new snippet
// POST /snippets
router.post( router.post(
"/", "/folders",
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
const { name, content, description } = req.body; const { name, color, icon } = req.body;
if ( if (!isNonEmptyString(userId) || !isNonEmptyString(name)) {
!isNonEmptyString(userId) || authLogger.warn("Invalid snippet folder creation data", {
!isNonEmptyString(name) || operation: "snippet_folder_create",
!isNonEmptyString(content)
) {
authLogger.warn("Invalid snippet creation data validation failed", {
operation: "snippet_create",
userId, userId,
hasName: !!name, hasName: !!name,
hasContent: !!content,
}); });
return res.status(400).json({ error: "Name and content are required" }); return res.status(400).json({ error: "Folder name is required" });
} }
try { try {
const existing = await db
.select()
.from(snippetFolders)
.where(
and(eq(snippetFolders.userId, userId), eq(snippetFolders.name, name)),
);
if (existing.length > 0) {
return res
.status(409)
.json({ error: "Folder with this name already exists" });
}
const insertData = { const insertData = {
userId, userId,
name: name.trim(), name: name.trim(),
content: content.trim(), color: color?.trim() || null,
description: description?.trim() || null, icon: icon?.trim() || null,
}; };
const result = await db.insert(snippets).values(insertData).returning(); const result = await db
.insert(snippetFolders)
.values(insertData)
.returning();
authLogger.success(`Snippet created: ${name} by user ${userId}`, { authLogger.success(`Snippet folder created: ${name} by user ${userId}`, {
operation: "snippet_create_success", operation: "snippet_folder_create_success",
userId, userId,
snippetId: result[0].id,
name, name,
}); });
res.status(201).json(result[0]); res.status(201).json(result[0]);
} catch (err) { } catch (err) {
authLogger.error("Failed to create snippet", err); authLogger.error("Failed to create snippet folder", err);
res.status(500).json({ res.status(500).json({
error: err instanceof Error ? err.message : "Failed to create snippet", error:
err instanceof Error
? err.message
: "Failed to create snippet folder",
}); });
} }
}, },
); );
// Update a snippet // Update snippet folder metadata (color, icon)
// PUT /snippets/:id // PUT /snippets/folders/:name/metadata
router.put( router.put(
"/:id", "/folders/:name/metadata",
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { name } = req.params;
const updateData = req.body; const { color, icon } = req.body;
if (!isNonEmptyString(userId) || !id) { if (!isNonEmptyString(userId) || !name) {
authLogger.warn("Invalid request for snippet update"); authLogger.warn("Invalid request for snippet folder metadata update");
return res.status(400).json({ error: "Invalid request" }); return res.status(400).json({ error: "Invalid request" });
} }
try { try {
const existing = await db const existing = await db
.select() .select()
.from(snippets) .from(snippetFolders)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId))); .where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, decodeURIComponent(name)),
),
);
if (existing.length === 0) { if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" }); return res.status(404).json({ error: "Folder not found" });
} }
const updateFields: Partial<{ const updateFields: Partial<{
color: string | null;
icon: string | null;
updatedAt: ReturnType<typeof sql.raw>; updatedAt: ReturnType<typeof sql.raw>;
name: string;
content: string;
description: string | null;
}> = { }> = {
updatedAt: sql`CURRENT_TIMESTAMP`, updatedAt: sql`CURRENT_TIMESTAMP`,
}; };
if (updateData.name !== undefined) if (color !== undefined) updateFields.color = color?.trim() || null;
updateFields.name = updateData.name.trim(); if (icon !== undefined) updateFields.icon = icon?.trim() || null;
if (updateData.content !== undefined)
updateFields.content = updateData.content.trim();
if (updateData.description !== undefined)
updateFields.description = updateData.description?.trim() || null;
await db await db
.update(snippets) .update(snippetFolders)
.set(updateFields) .set(updateFields)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId))); .where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, decodeURIComponent(name)),
),
);
const updated = await db const updated = await db
.select() .select()
.from(snippets) .from(snippetFolders)
.where(eq(snippets.id, parseInt(id))); .where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, decodeURIComponent(name)),
),
);
authLogger.success( authLogger.success(
`Snippet updated: ${updated[0].name} by user ${userId}`, `Snippet folder metadata updated: ${name} by user ${userId}`,
{ {
operation: "snippet_update_success", operation: "snippet_folder_metadata_update_success",
userId, userId,
snippetId: parseInt(id), name,
name: updated[0].name,
}, },
); );
res.json(updated[0]); res.json(updated[0]);
} catch (err) { } catch (err) {
authLogger.error("Failed to update snippet", err); authLogger.error("Failed to update snippet folder metadata", err);
res.status(500).json({ res.status(500).json({
error: err instanceof Error ? err.message : "Failed to update snippet", error:
err instanceof Error
? err.message
: "Failed to update snippet folder metadata",
}); });
} }
}, },
); );
// Delete a snippet // Rename snippet folder
// DELETE /snippets/:id // PUT /snippets/folders/rename
router.delete( router.put(
"/:id", "/folders/rename",
authenticateJWT, authenticateJWT,
requireDataAccess, requireDataAccess,
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params; const { oldName, newName } = req.body;
if (!isNonEmptyString(userId) || !id) { if (
authLogger.warn("Invalid request for snippet delete"); !isNonEmptyString(userId) ||
!isNonEmptyString(oldName) ||
!isNonEmptyString(newName)
) {
authLogger.warn("Invalid request for snippet folder rename");
return res.status(400).json({ error: "Invalid request" }); return res.status(400).json({ error: "Invalid request" });
} }
try { try {
const existing = await db const existing = await db
.select() .select()
.from(snippets) .from(snippetFolders)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId))); .where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, oldName),
),
);
if (existing.length === 0) { if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" }); return res.status(404).json({ error: "Folder not found" });
}
const nameExists = await db
.select()
.from(snippetFolders)
.where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, newName),
),
);
if (nameExists.length > 0) {
return res
.status(409)
.json({ error: "Folder with new name already exists" });
} }
await db await db
.delete(snippets) .update(snippetFolders)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId))); .set({ name: newName, updatedAt: sql`CURRENT_TIMESTAMP` })
.where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, oldName),
),
);
await db
.update(snippets)
.set({ folder: newName })
.where(and(eq(snippets.userId, userId), eq(snippets.folder, oldName)));
authLogger.success( authLogger.success(
`Snippet deleted: ${existing[0].name} by user ${userId}`, `Snippet folder renamed: ${oldName} -> ${newName} by user ${userId}`,
{ {
operation: "snippet_delete_success", operation: "snippet_folder_rename_success",
userId, userId,
snippetId: parseInt(id), oldName,
name: existing[0].name, newName,
},
);
res.json({ success: true, oldName, newName });
} catch (err) {
authLogger.error("Failed to rename snippet folder", err);
res.status(500).json({
error:
err instanceof Error
? err.message
: "Failed to rename snippet folder",
});
}
},
);
// Delete snippet folder
// DELETE /snippets/folders/:name
router.delete(
"/folders/:name",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { name } = req.params;
if (!isNonEmptyString(userId) || !name) {
authLogger.warn("Invalid request for snippet folder delete");
return res.status(400).json({ error: "Invalid request" });
}
try {
const folderName = decodeURIComponent(name);
await db
.update(snippets)
.set({ folder: null })
.where(
and(eq(snippets.userId, userId), eq(snippets.folder, folderName)),
);
await db
.delete(snippetFolders)
.where(
and(
eq(snippetFolders.userId, userId),
eq(snippetFolders.name, folderName),
),
);
authLogger.success(
`Snippet folder deleted: ${folderName} by user ${userId}`,
{
operation: "snippet_folder_delete_success",
userId,
name: folderName,
}, },
); );
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
authLogger.error("Failed to delete snippet", err); authLogger.error("Failed to delete snippet folder", err);
res.status(500).json({ res.status(500).json({
error: err instanceof Error ? err.message : "Failed to delete snippet", error:
err instanceof Error
? err.message
: "Failed to delete snippet folder",
});
}
},
);
// Reorder snippets (bulk update)
// PUT /snippets/reorder
router.put(
"/reorder",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { snippets: snippetUpdates } = req.body;
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippet reorder");
return res.status(400).json({ error: "Invalid userId" });
}
if (!Array.isArray(snippetUpdates) || snippetUpdates.length === 0) {
authLogger.warn("Invalid snippet reorder data", {
operation: "snippet_reorder",
userId,
});
return res
.status(400)
.json({ error: "snippets array is required and must not be empty" });
}
try {
for (const update of snippetUpdates) {
const { id, order, folder } = update;
if (!id || order === undefined) {
continue;
}
const updateFields: Partial<{
order: number;
folder: string | null;
}> = {
order,
};
if (folder !== undefined) {
updateFields.folder = folder?.trim() || null;
}
await db
.update(snippets)
.set(updateFields)
.where(and(eq(snippets.id, id), eq(snippets.userId, userId)));
}
authLogger.success(`Snippets reordered by user ${userId}`, {
operation: "snippet_reorder_success",
userId,
count: snippetUpdates.length,
});
res.json({ success: true, updated: snippetUpdates.length });
} catch (err) {
authLogger.error("Failed to reorder snippets", err);
res.status(500).json({
error:
err instanceof Error ? err.message : "Failed to reorder snippets",
}); });
} }
}, },
@@ -514,4 +662,274 @@ router.post(
}, },
); );
// Get all snippets for the authenticated user
// GET /snippets
router.get(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for snippets fetch");
return res.status(400).json({ error: "Invalid userId" });
}
try {
const result = await db
.select()
.from(snippets)
.where(eq(snippets.userId, userId))
.orderBy(
sql`CASE WHEN ${snippets.folder} IS NULL OR ${snippets.folder} = '' THEN 0 ELSE 1 END`,
asc(snippets.folder),
asc(snippets.order),
desc(snippets.updatedAt),
);
res.json(result);
} catch (err) {
authLogger.error("Failed to fetch snippets", err);
res.status(500).json({ error: "Failed to fetch snippets" });
}
},
);
// Get a specific snippet by ID
// GET /snippets/:id
router.get(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const snippetId = parseInt(id, 10);
if (!isNonEmptyString(userId) || isNaN(snippetId)) {
authLogger.warn("Invalid request for snippet fetch: invalid ID", {
userId,
id,
});
return res.status(400).json({ error: "Invalid request parameters" });
}
try {
const result = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (result.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
res.json(result[0]);
} catch (err) {
authLogger.error("Failed to fetch snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to fetch snippet",
});
}
},
);
// Create a new snippet
// POST /snippets
router.post(
"/",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { name, content, description, folder, order } = req.body;
if (
!isNonEmptyString(userId) ||
!isNonEmptyString(name) ||
!isNonEmptyString(content)
) {
authLogger.warn("Invalid snippet creation data validation failed", {
operation: "snippet_create",
userId,
hasName: !!name,
hasContent: !!content,
});
return res.status(400).json({ error: "Name and content are required" });
}
try {
let snippetOrder = order;
if (snippetOrder === undefined || snippetOrder === null) {
const folderValue = folder?.trim() || "";
const maxOrderResult = await db
.select({ maxOrder: sql<number>`MAX(${snippets.order})` })
.from(snippets)
.where(
and(
eq(snippets.userId, userId),
folderValue
? eq(snippets.folder, folderValue)
: sql`(${snippets.folder} IS NULL OR ${snippets.folder} = '')`,
),
);
const maxOrder = maxOrderResult[0]?.maxOrder ?? -1;
snippetOrder = maxOrder + 1;
}
const insertData = {
userId,
name: name.trim(),
content: content.trim(),
description: description?.trim() || null,
folder: folder?.trim() || null,
order: snippetOrder,
};
const result = await db.insert(snippets).values(insertData).returning();
authLogger.success(`Snippet created: ${name} by user ${userId}`, {
operation: "snippet_create_success",
userId,
snippetId: result[0].id,
name,
});
res.status(201).json(result[0]);
} catch (err) {
authLogger.error("Failed to create snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to create snippet",
});
}
},
);
// Update a snippet
// PUT /snippets/:id
router.put(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const updateData = req.body;
if (!isNonEmptyString(userId) || !id) {
authLogger.warn("Invalid request for snippet update");
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
const updateFields: Partial<{
updatedAt: ReturnType<typeof sql.raw>;
name: string;
content: string;
description: string | null;
folder: string | null;
order: number;
}> = {
updatedAt: sql`CURRENT_TIMESTAMP`,
};
if (updateData.name !== undefined)
updateFields.name = updateData.name.trim();
if (updateData.content !== undefined)
updateFields.content = updateData.content.trim();
if (updateData.description !== undefined)
updateFields.description = updateData.description?.trim() || null;
if (updateData.folder !== undefined)
updateFields.folder = updateData.folder?.trim() || null;
if (updateData.order !== undefined) updateFields.order = updateData.order;
await db
.update(snippets)
.set(updateFields)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
const updated = await db
.select()
.from(snippets)
.where(eq(snippets.id, parseInt(id)));
authLogger.success(
`Snippet updated: ${updated[0].name} by user ${userId}`,
{
operation: "snippet_update_success",
userId,
snippetId: parseInt(id),
name: updated[0].name,
},
);
res.json(updated[0]);
} catch (err) {
authLogger.error("Failed to update snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to update snippet",
});
}
},
);
// Delete a snippet
// DELETE /snippets/:id
router.delete(
"/:id",
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
authLogger.warn("Invalid request for snippet delete");
return res.status(400).json({ error: "Invalid request" });
}
try {
const existing = await db
.select()
.from(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
if (existing.length === 0) {
return res.status(404).json({ error: "Snippet not found" });
}
await db
.delete(snippets)
.where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
authLogger.success(
`Snippet deleted: ${existing[0].name} by user ${userId}`,
{
operation: "snippet_delete_success",
userId,
snippetId: parseInt(id),
name: existing[0].name,
},
);
res.json({ success: true });
} catch (err) {
authLogger.error("Failed to delete snippet", err);
res.status(500).json({
error: err instanceof Error ? err.message : "Failed to delete snippet",
});
}
},
);
export default router; export default router;

View File

@@ -506,6 +506,8 @@ export interface Snippet {
name: string; name: string;
content: string; content: string;
description?: string; description?: string;
folder?: string;
order?: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -514,6 +516,18 @@ export interface SnippetData {
name: string; name: string;
content: string; content: string;
description?: string; description?: string;
folder?: string;
order?: number;
}
export interface SnippetFolder {
id: number;
userId: string;
name: string;
color?: string;
icon?: string;
createdAt: string;
updatedAt: string;
} }
// ============================================================================ // ============================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -2874,6 +2874,82 @@ export async function executeSnippet(
} }
} }
export async function reorderSnippets(
snippets: Array<{ id: number; order: number; folder?: string }>,
): Promise<{ success: boolean; updated: number }> {
try {
const response = await authApi.put("/snippets/reorder", { snippets });
return response.data;
} catch (error) {
throw handleApiError(error, "reorder snippets");
}
}
export async function getSnippetFolders(): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/snippets/folders");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch snippet folders");
}
}
export async function createSnippetFolder(folderData: {
name: string;
color?: string;
icon?: string;
}): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/snippets/folders", folderData);
return response.data;
} catch (error) {
throw handleApiError(error, "create snippet folder");
}
}
export async function updateSnippetFolderMetadata(
folderName: string,
metadata: { color?: string; icon?: string },
): Promise<Record<string, unknown>> {
try {
const response = await authApi.put(
`/snippets/folders/${encodeURIComponent(folderName)}/metadata`,
metadata,
);
return response.data;
} catch (error) {
throw handleApiError(error, "update snippet folder metadata");
}
}
export async function renameSnippetFolder(
oldName: string,
newName: string,
): Promise<{ success: boolean; oldName: string; newName: string }> {
try {
const response = await authApi.put("/snippets/folders/rename", {
oldName,
newName,
});
return response.data;
} catch (error) {
throw handleApiError(error, "rename snippet folder");
}
}
export async function deleteSnippetFolder(
folderName: string,
): Promise<{ success: boolean }> {
try {
const response = await authApi.delete(
`/snippets/folders/${encodeURIComponent(folderName)}`,
);
return response.data;
} catch (error) {
throw handleApiError(error, "delete snippet folder");
}
}
// ============================================================================ // ============================================================================
// HOMEPAGE API // HOMEPAGE API
// ============================================================================ // ============================================================================