diff --git a/package-lock.json b/package-lock.json index a082bc61a..3600f1e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@clerk/backend": "^3.4.1", "@clerk/clerk-js": "^6.7.5", "@libsql/client": "^0.17.3", "@solidjs/router": "^0.16.1", @@ -353,6 +354,20 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/@clerk/backend": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.4.1.tgz", + "integrity": "sha512-+Tgo1uPEFpBRvyFW3JtPbrTMRgiP+pWBo9gi2tTB0AxEqR2I/kSYy5l3+KqWciUpbVZtVvLXm1j+NEE2WEG+jg==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^4.8.5", + "standardwebhooks": "^1.0.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.9.0" + } + }, "node_modules/@clerk/clerk-js": { "version": "6.7.5", "resolved": "https://registry.npmjs.org/@clerk/clerk-js/-/clerk-js-6.7.5.tgz", @@ -392,9 +407,9 @@ } }, "node_modules/@clerk/shared": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.8.3.tgz", - "integrity": "sha512-HZViZBCTfOR2OreSBDMXcIRPgYiiYCE+GCCPrpjq/ZPcA6OsGiRCIQgUoGgGdAoFgr6Hk0TT00hnVK7g0qRKqQ==", + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.8.5.tgz", + "integrity": "sha512-YxgUWHoKEXEbRPWPEcB2Q0o+NJkDc0/zQRp4QCsnGIM5e32hlBUwxcYpyDjDlZ2lYB+GUXHuEc3KETnxWGp26g==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3026,6 +3041,12 @@ "solid-js": "^1.8.6" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@stripe/stripe-js": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.6.0.tgz", @@ -6173,6 +6194,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fast-stable-stringify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", @@ -9945,6 +9972,16 @@ "node": ">=8" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", diff --git a/package.json b/package.json index a22a60978..70bb1b39b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "tauri:icons": "bash src-tauri/generate-icons.sh" }, "dependencies": { + "@clerk/backend": "^3.4.1", "@clerk/clerk-js": "^6.7.5", "@libsql/client": "^0.17.3", "@solidjs/router": "^0.16.1", diff --git a/server/trpc/index.ts b/server/trpc/index.ts index 05106eefd..f70f30a54 100644 --- a/server/trpc/index.ts +++ b/server/trpc/index.ts @@ -4,6 +4,10 @@ import { revisionsRouter } from './revisions-router'; import type { TRPCContext } from './types'; import type { TRPCError } from '@trpc/server'; import { t } from './router'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import Database from 'better-sqlite3'; +import { projects, characters, scenes, characterRelationships, sceneCharacters } from '../../src/db/schema'; +import { verifyToken } from '@clerk/backend'; // App router combining all routers export const appRouter = t.router({ @@ -13,13 +17,42 @@ export const appRouter = t.router({ export type AppRouter = typeof appRouter; +// Database instance (shared for now - should come from config) +let dbInstance: ReturnType | null = null; + +function getDb() { + if (dbInstance) return dbInstance; + const sqlite = new Database('./data/frenocorp.db'); + dbInstance = drizzle(sqlite); + return dbInstance; +} + // Create tRPC HTTP server export function createTRPCServer(port: number = 8080) { const server = createHTTPServer({ router: appRouter, - createContext: async (): Promise => { + createContext: async ({ req }): Promise => { + const authHeader = req.headers.authorization; + let userId: number | undefined = undefined; + + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); + try { + const clerkSecretKey = process.env.CLERK_SECRET_KEY; + if (!clerkSecretKey) { + console.warn('CLERK_SECRET_KEY not set, skipping token verification'); + } else { + const payload = await verifyToken(token, { secretKey: clerkSecretKey }); + userId = payload.sub ? parseInt(payload.sub, 10) : undefined; + } + } catch (error) { + console.error('Failed to verify Clerk token:', error); + } + } + return { - userId: undefined, + userId, + db: getDb(), }; }, onError: ({ error, path }: { error: TRPCError; path: string | undefined }) => { diff --git a/server/trpc/project-router.ts b/server/trpc/project-router.ts index cc92aaf4d..7b578a03f 100644 --- a/server/trpc/project-router.ts +++ b/server/trpc/project-router.ts @@ -1,4 +1,4 @@ -import { publicProcedure, protectedProcedure, projectProcedure } from './router'; +import { publicProcedure, protectedProcedure, projectProcedure, TRPCError } from './router'; import { z } from 'zod'; import { eq, and, or, like, sql, inArray } from 'drizzle-orm'; import type { DrizzleDB } from '../../src/db/config/migrations'; @@ -66,10 +66,10 @@ async function verifyProjectOwnership( const project = projectRows[0]; if (!project) { - throw new Error(`Project ${projectId} not found`); + throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${projectId} not found` }); } if (project.ownerId !== userId) { - throw new Error(`You do not have access to project ${projectId}`); + throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${projectId}` }); } return project; } @@ -91,10 +91,10 @@ export const projectRouter = { .where(eq(projects.id, input.id)); const project = rows[0]; if (!project) { - throw new Error(`Project ${input.id} not found`); + throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${input.id} not found` }); } if (project.ownerId !== ctx.userId && !project.isPublic) { - throw new Error(`You do not have access to project ${input.id}`); + throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${input.id}` }); } return project; }), diff --git a/server/trpc/revisions-router.test.ts b/server/trpc/revisions-router.test.ts index 38df0c711..5560b5727 100644 --- a/server/trpc/revisions-router.test.ts +++ b/server/trpc/revisions-router.test.ts @@ -1,50 +1,57 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { appRouter } from './index'; +import { getTestDb, resetTestDb } from './test-setup'; +import { resetInMemoryState } from './revisions-router'; +import type { TRPCContext } from './types'; describe('revisionsRouter', () => { - const ctx = { userId: '123e4567-e89b-12d3-a456-426614174000' }; + let ctx: TRPCContext; + let caller: ReturnType; + + beforeEach(async () => { + await resetTestDb(); + resetInMemoryState(); + const db = await getTestDb(); + ctx = { userId: 1, db }; + caller = appRouter.createCaller(ctx); + }); describe('createRevision', () => { it('should create a revision with version 1', async () => { - const result = await appRouter.revisions.createRevision.mutate({ - input: { - scriptId: 1, - title: 'Initial draft', - content: 'FADE IN:\n\nINT. ROOM - DAY', - authorId: 1, - }, - ctx, + const result = await caller.revisions.createRevision({ + scriptId: 1, + title: 'Initial draft', + content: 'FADE IN:\n\nINT. ROOM - DAY', }); expect(result.versionNumber).toBe(1); expect(result.branchName).toBe('main'); expect(result.status).toBe('draft'); + expect(result.authorId).toBe(1); }); it('should increment version number for same script', async () => { - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'v1', content: 'content1', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'v1', + content: 'content1', }); - const result = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'v2', content: 'content2', authorId: 1 }, - ctx, + const result = await caller.revisions.createRevision({ + scriptId: 1, + title: 'v2', + content: 'content2', }); expect(result.versionNumber).toBe(2); }); it('should support custom branch', async () => { - const result = await appRouter.revisions.createRevision.mutate({ - input: { - scriptId: 1, - title: 'Branch revision', - content: 'branch content', - branchName: 'feature-act2', - authorId: 1, - }, - ctx, + const result = await caller.revisions.createRevision({ + scriptId: 1, + title: 'Branch revision', + content: 'branch content', + branchName: 'feature-act2', }); expect(result.branchName).toBe('feature-act2'); @@ -53,27 +60,28 @@ describe('revisionsRouter', () => { describe('listRevisions', () => { it('should return empty array for unknown script', async () => { - const result = await appRouter.revisions.listRevisions.query({ - input: { scriptId: 999 }, - ctx, - }); + const result = await caller.revisions.listRevisions({ scriptId: 999 }); expect(result).toEqual([]); }); it('should filter by branch', async () => { - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'main v1', content: 'main', branchName: 'main', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'main v1', + content: 'main', + branchName: 'main', }); - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'feature v1', content: 'feature', branchName: 'feature', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'feature v1', + content: 'feature', + branchName: 'feature', }); - const mainRevisions = await appRouter.revisions.listRevisions.query({ - input: { scriptId: 1, branchName: 'main' }, - ctx, + const mainRevisions = await caller.revisions.listRevisions({ + scriptId: 1, + branchName: 'main', }); expect(mainRevisions).toHaveLength(1); @@ -83,32 +91,33 @@ describe('revisionsRouter', () => { describe('acceptRevision', () => { it('should accept a revision', async () => { - const created = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'To accept', content: 'content', authorId: 1 }, - ctx, + const created = await caller.revisions.createRevision({ + scriptId: 1, + title: 'To accept', + content: 'content', }); - const result = await appRouter.revisions.acceptRevision.mutate({ - input: { revisionId: created.id, reviewedById: 2 }, - ctx, + const result = await caller.revisions.acceptRevision({ + revisionId: created.id, }); expect(result.status).toBe('accepted'); - expect(result.reviewedById).toBe(2); + expect(result.reviewedById).toBe(1); expect(result.reviewedAt).toBeDefined(); }); }); describe('rejectRevision', () => { it('should reject a revision with reason', async () => { - const created = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'To reject', content: 'content', authorId: 1 }, - ctx, + const created = await caller.revisions.createRevision({ + scriptId: 1, + title: 'To reject', + content: 'content', }); - const result = await appRouter.revisions.rejectRevision.mutate({ - input: { revisionId: created.id, reviewedById: 2, reason: 'Needs more work on dialogue' }, - ctx, + const result = await caller.revisions.rejectRevision({ + revisionId: created.id, + reason: 'Needs more work on dialogue', }); expect(result.status).toBe('rejected'); @@ -118,19 +127,21 @@ describe('revisionsRouter', () => { describe('rollbackToRevision', () => { it('should create a new revision with old content', async () => { - const original = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'Original', content: 'original content', authorId: 1 }, - ctx, + const original = await caller.revisions.createRevision({ + scriptId: 1, + title: 'Original', + content: 'original content', }); - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'Changed', content: 'changed content', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'Changed', + content: 'changed content', }); - const rollback = await appRouter.revisions.rollbackToRevision.mutate({ - input: { scriptId: 1, revisionId: original.id, authorId: 1 }, - ctx, + const rollback = await caller.revisions.rollbackToRevision({ + scriptId: 1, + revisionId: original.id, }); expect(rollback.content).toBe('original content'); @@ -141,19 +152,21 @@ describe('revisionsRouter', () => { describe('compareRevisions', () => { it('should compare two revisions', async () => { - const rev1 = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'v1', content: 'line1\nline2\nline3', authorId: 1 }, - ctx, + const rev1 = await caller.revisions.createRevision({ + scriptId: 1, + title: 'v1', + content: 'line1\nline2\nline3', }); - const rev2 = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'v2', content: 'line1\nchanged\nline3', authorId: 1 }, - ctx, + const rev2 = await caller.revisions.createRevision({ + scriptId: 1, + title: 'v2', + content: 'line1\nchanged\nline3', }); - const result = await appRouter.revisions.compareRevisions.query({ - input: { baseRevisionId: rev1.id, targetRevisionId: rev2.id }, - ctx, + const result = await caller.revisions.compareRevisions({ + baseRevisionId: rev1.id, + targetRevisionId: rev2.id, }); expect(result.diff.modifications).toBe(1); @@ -164,20 +177,19 @@ describe('revisionsRouter', () => { describe('getTimeline', () => { it('should return timeline entries in chronological order', async () => { - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'First', content: 'first', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'First', + content: 'first', }); - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'Second', content: 'second', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'Second', + content: 'second', }); - const timeline = await appRouter.revisions.getTimeline.query({ - input: { scriptId: 1 }, - ctx, - }); + const timeline = await caller.revisions.getTimeline({ scriptId: 1 }); expect(timeline).toHaveLength(2); expect(timeline[0]!.revision.title).toBe('First'); @@ -187,20 +199,18 @@ describe('revisionsRouter', () => { describe('getBranches', () => { it('should return branch information', async () => { - await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'main v1', content: 'main', authorId: 1 }, - ctx, + await caller.revisions.createRevision({ + scriptId: 1, + title: 'main v1', + content: 'main', }); - await appRouter.revisions.createBranch.mutate({ - input: { scriptId: 1, branchName: 'feature', authorId: 1 }, - ctx, + await caller.revisions.createBranch({ + scriptId: 1, + branchName: 'feature', }); - const branches = await appRouter.revisions.getBranches.query({ - input: { scriptId: 1 }, - ctx, - }); + const branches = await caller.revisions.getBranches({ scriptId: 1 }); expect(branches).toHaveLength(2); const branchNames = branches.map((b: any) => b.branchName); @@ -211,23 +221,18 @@ describe('revisionsRouter', () => { describe('deleteRevision', () => { it('should delete a revision', async () => { - const created = await appRouter.revisions.createRevision.mutate({ - input: { scriptId: 1, title: 'To delete', content: 'content', authorId: 1 }, - ctx, + const created = await caller.revisions.createRevision({ + scriptId: 1, + title: 'To delete', + content: 'content', }); - const result = await appRouter.revisions.deleteRevision.mutate({ - input: { id: created.id }, - ctx, - }); + const result = await caller.revisions.deleteRevision({ id: created.id }); expect(result.success).toBe(true); await expect( - appRouter.revisions.getRevision.query({ - input: { id: created.id }, - ctx, - }) + caller.revisions.getRevision({ id: created.id }) ).rejects.toThrow(); }); }); diff --git a/server/trpc/revisions-router.ts b/server/trpc/revisions-router.ts index 0d93ea8af..29a2f87fa 100644 --- a/server/trpc/revisions-router.ts +++ b/server/trpc/revisions-router.ts @@ -119,6 +119,14 @@ function getLatestVersionForScript(scriptId: number, branchName: string): number return maxVersion; } +// Export reset function for testing +export function resetInMemoryState() { + revisions.clear(); + revisionChanges.clear(); + revisionIdCounter = 0; + changeIdCounter = 0; +} + export const revisionsRouter = { listRevisions: protectedProcedure .input(z.object({ @@ -153,9 +161,12 @@ export const revisionsRouter = { content: z.string(), branchName: z.string().default('main'), parentRevisionId: z.number().int().positive().optional(), - authorId: z.number().int().positive(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + const nextVersion = getLatestVersionForScript( input.scriptId, input.branchName @@ -170,7 +181,7 @@ export const revisionsRouter = { title: input.title, summary: input.summary || null, content: input.content, - authorId: input.authorId, + authorId: ctx.userId, status: 'draft' as const, reviewedById: null, reviewedAt: null, @@ -306,9 +317,12 @@ export const revisionsRouter = { acceptRevision: protectedProcedure .input(z.object({ revisionId: z.number().int().positive(), - reviewedById: z.number().int().positive(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + const revision = revisions.get(input.revisionId); if (!revision) { throw new Error(`Revision ${input.revisionId} not found`); @@ -317,7 +331,7 @@ export const revisionsRouter = { const updated = { ...revision, status: 'accepted' as const, - reviewedById: input.reviewedById, + reviewedById: ctx.userId, reviewedAt: new Date(), updatedAt: new Date(), }; @@ -329,10 +343,13 @@ export const revisionsRouter = { rejectRevision: protectedProcedure .input(z.object({ revisionId: z.number().int().positive(), - reviewedById: z.number().int().positive(), reason: z.string().max(1000).optional(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + const revision = revisions.get(input.revisionId); if (!revision) { throw new Error(`Revision ${input.revisionId} not found`); @@ -341,7 +358,7 @@ export const revisionsRouter = { const updated = { ...revision, status: 'rejected' as const, - reviewedById: input.reviewedById, + reviewedById: ctx.userId, reviewedAt: new Date(), summary: input.reason ? (revision.summary || '') + '\n[Rejected: ' + input.reason + ']' @@ -357,9 +374,12 @@ export const revisionsRouter = { .input(z.object({ scriptId: z.number().int().positive(), revisionId: z.number().int().positive(), - authorId: z.number().int().positive(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + const targetRevision = revisions.get(input.revisionId); if (!targetRevision) { throw new Error(`Revision ${input.revisionId} not found`); @@ -383,7 +403,7 @@ export const revisionsRouter = { title: `Rollback to v${targetRevision.versionNumber}: ${targetRevision.title}`, summary: `Rolled back to revision ${targetRevision.id}`, content: targetRevision.content, - authorId: input.authorId, + authorId: ctx.userId, status: 'draft' as const, reviewedById: null, reviewedAt: null, @@ -460,9 +480,12 @@ export const revisionsRouter = { scriptId: z.number().int().positive(), branchName: z.string().min(1), fromRevisionId: z.number().int().positive().optional(), - authorId: z.number().int().positive(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + const existing = Array.from(revisions.values()) .some(r => r.scriptId === input.scriptId && r.branchName === input.branchName); @@ -500,7 +523,7 @@ export const revisionsRouter = { title: `Branch: ${input.branchName}`, summary: null, content: sourceContent, - authorId: input.authorId, + authorId: ctx.userId, status: 'draft' as const, reviewedById: null, reviewedAt: null, @@ -517,9 +540,12 @@ export const revisionsRouter = { scriptId: z.number().int().positive(), sourceBranch: z.string(), targetBranch: z.string(), - authorId: z.number().int().positive(), })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (!ctx.userId) { + throw new Error('User not authenticated'); + } + if (input.sourceBranch === input.targetBranch) { throw new Error('Cannot merge a branch into itself'); } @@ -547,7 +573,7 @@ export const revisionsRouter = { title: `Merge from '${input.sourceBranch}'`, summary: `Merged ${input.sourceBranch} into ${input.targetBranch}`, content: sourceContent, - authorId: input.authorId, + authorId: ctx.userId, status: 'draft' as const, reviewedById: null, reviewedAt: null, diff --git a/server/trpc/types.ts b/server/trpc/types.ts index 772d2645d..433211f47 100644 --- a/server/trpc/types.ts +++ b/server/trpc/types.ts @@ -161,5 +161,5 @@ export const SceneListSchema = z.array(SceneSchema); export interface TRPCContext { userId?: number; projectId?: number; - db?: import('../../src/db/config/migrations').DrizzleDB; + db?: ReturnType; } diff --git a/server/websocket/index.ts b/server/websocket/index.ts index e5fa0de87..1dc91da6f 100644 --- a/server/websocket/index.ts +++ b/server/websocket/index.ts @@ -63,10 +63,15 @@ export async function startServer(config: ServerConfig) { // If run directly, start the server if (require.main === module) { + const jwtSecret = process.env.JWT_SECRET; + if (!jwtSecret) { + throw new Error('JWT_SECRET environment variable is required. Please set it before starting the server.'); + } + const config: ServerConfig = { port: parseInt(process.env.WS_PORT || '8080', 10), - jwtSecret: process.env.JWT_SECRET || 'dev-secret', - enableAuth: process.env.ENABLE_AUTH === 'true', + jwtSecret, + enableAuth: process.env.ENABLE_AUTH !== 'false', }; startServer(config).catch((error) => { diff --git a/src/db/config/backup.ts b/src/db/config/backup.ts index 9ee88eb61..a40a89129 100644 --- a/src/db/config/backup.ts +++ b/src/db/config/backup.ts @@ -41,9 +41,16 @@ export class DatabaseBackupManager { "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" ); + const tableNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + for (const table of tables) { + if (!tableNamePattern.test(table)) { + console.warn(`Skipping invalid table name: ${table}`); + continue; + } + const data = await this.dbManager.query>( - `SELECT * FROM ${table}` + `SELECT * FROM "${table}"` ); console.log(`Backed up ${table}: ${data.length} rows`);