diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index ebda7c3..8a2701a 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -65,37 +65,9 @@ const app = new Hono() }) .onError((err, c) => { console.error('Unhandled error:', err); - return c.json({ error: err.message }, 500); + return c.json({ error: 'Internal server error' }, 500); }) .get('/health', (c) => c.json({ status: 'ok' })) - .get('/migrate', async (c) => { - try { - const fs = await import('fs'); - const pathMod = await import('path'); - const { sql: dsql } = await import('drizzle-orm'); - const folder = pathMod.resolve(process.cwd(), 'packages/db/src/migrations'); - const journal = JSON.parse(fs.readFileSync(pathMod.join(folder, 'meta', '_journal.json'), 'utf8')); - const results: string[] = []; - for (const entry of journal.entries) { - const sqlFile = pathMod.join(folder, `${entry.tag}.sql`); - if (!fs.existsSync(sqlFile)) { results.push(`skip: ${entry.tag} (not found)`); continue; } - const rawSql = fs.readFileSync(sqlFile, 'utf8'); - const statements = rawSql.split('--> statement-breakpoint').map((s: string) => s.trim()).filter(Boolean); - for (const stmt of statements) { - try { - await db.execute(dsql.raw(stmt)); - } catch (err: any) { - if (err.message?.includes('already exists') || err.message?.includes('duplicate')) continue; - results.push(`error in ${entry.tag}: ${err.message?.slice(0, 150)}`); - } - } - results.push(`ok: ${entry.tag}`); - } - return c.json({ status: 'ok', results }); - } catch (err: any) { - return c.json({ status: 'error', message: err.message }, 500); - } - }) .basePath('/api/v1') .route('/auth', authRoutes) .route('/projects', projectRoutes) diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 9525229..10668ea 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -16,6 +16,7 @@ async function createSession(c: any, db: any, userId: string) { setCookie(c, 'session', sessionToken, { httpOnly: true, sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', path: '/', maxAge: 30 * 24 * 60 * 60, }); @@ -64,11 +65,12 @@ export const authRoutes = new Hono() const db = c.get('db'); const token = generateToken(); + const tokenHash = await hashToken(token); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 min await db.insert(magicLinks).values({ email, - token, + token: tokenHash, expiresAt, }); @@ -81,10 +83,11 @@ export const authRoutes = new Hono() const { token } = c.req.valid('json'); const db = c.get('db'); + const tokenHash = await hashToken(token); const [link] = await db .select() .from(magicLinks) - .where(eq(magicLinks.token, token)) + .where(eq(magicLinks.token, tokenHash)) .limit(1); if (!link || link.expiresAt < new Date() || link.usedAt) { diff --git a/apps/api/src/routes/comments.ts b/apps/api/src/routes/comments.ts index b6f7080..25f521a 100644 --- a/apps/api/src/routes/comments.ts +++ b/apps/api/src/routes/comments.ts @@ -158,15 +158,29 @@ export const commentRoutes = new Hono() // Resolve comment .post('/:id/resolve', async (c) => { const db = c.get('db'); + const userId = c.get('userId'); const commentId = c.req.param('id'); + const [comment] = await db.select().from(comments).where(eq(comments.id, commentId)).limit(1); + if (!comment) return c.json({ error: 'Not found' }, 404); + + const [version] = await db.select().from(versions).where(eq(versions.id, comment.versionId)).limit(1); + const [track] = await db.select().from(tracks).where(eq(tracks.id, version!.trackId)).limit(1); + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership || (!membership.canComment && !membership.canApprove)) { + return c.json({ error: 'Forbidden' }, 403); + } + const [updated] = await db .update(comments) .set({ resolvedAt: new Date() }) .where(eq(comments.id, commentId)) .returning(); - if (!updated) return c.json({ error: 'Not found' }, 404); - return c.json({ comment: updated }); }); diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts index 8991d9a..0b58137 100644 --- a/apps/api/src/routes/projects.ts +++ b/apps/api/src/routes/projects.ts @@ -266,7 +266,7 @@ export const projectRoutes = new Hono() const [updated] = await db .update(projectMembers) .set({ role: newRole, ...defaults }) - .where(eq(projectMembers.id, memberId)) + .where(and(eq(projectMembers.id, memberId), eq(projectMembers.projectId, projectId))) .returning(); return c.json({ member: updated }); @@ -294,7 +294,7 @@ export const projectRoutes = new Hono() return c.json({ error: 'Forbidden' }, 403); } - await db.delete(projectMembers).where(eq(projectMembers.id, memberId)); + await db.delete(projectMembers).where(and(eq(projectMembers.id, memberId), eq(projectMembers.projectId, projectId))); return c.json({ message: 'Member removed' }); }); diff --git a/apps/api/src/routes/versions.ts b/apps/api/src/routes/versions.ts index 6797686..2c7d907 100644 --- a/apps/api/src/routes/versions.ts +++ b/apps/api/src/routes/versions.ts @@ -81,6 +81,23 @@ export const versionRoutes = new Hono() const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1); if (!track) return c.json({ error: 'Not found' }, 404); + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership || !membership.canUpload) { + return c.json({ error: 'Forbidden' }, 403); + } + + const expectedPrefix = `projects/${track.projectId}/tracks/${trackId}/`; + if (!input.fileKey.startsWith(expectedPrefix)) { + return c.json({ error: 'Forbidden' }, 403); + } + // Get next version number const [latest] = await db .select({ maxVersion: sql`coalesce(max(${versions.versionNumber}), 0)` }) @@ -269,16 +286,20 @@ export const versionRoutes = new Hono() // Get stream URL .get('/:id/stream-url', async (c) => { const db = c.get('db'); + const userId = c.get('userId'); const versionId = c.req.param('id'); - const [version] = await db - .select() - .from(versions) - .where(eq(versions.id, versionId)) - .limit(1); - + const [version] = await db.select().from(versions).where(eq(versions.id, versionId)).limit(1); if (!version) return c.json({ error: 'Not found' }, 404); + const [track] = await db.select().from(tracks).where(eq(tracks.id, version.trackId)).limit(1); + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId))) + .limit(1); + if (!membership) return c.json({ error: 'Not found' }, 404); + const key = version.streamFileKey || version.originalFileKey; const url = await createDownloadUrl(key); @@ -288,16 +309,20 @@ export const versionRoutes = new Hono() // Get download URL .get('/:id/download-url', async (c) => { const db = c.get('db'); + const userId = c.get('userId'); const versionId = c.req.param('id'); - const [version] = await db - .select() - .from(versions) - .where(eq(versions.id, versionId)) - .limit(1); - + const [version] = await db.select().from(versions).where(eq(versions.id, versionId)).limit(1); if (!version) return c.json({ error: 'Not found' }, 404); + const [track] = await db.select().from(tracks).where(eq(tracks.id, version.trackId)).limit(1); + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId))) + .limit(1); + if (!membership) return c.json({ error: 'Not found' }, 404); + const url = await createDownloadUrl(version.originalFileKey); return c.json({ url }); }) @@ -305,17 +330,19 @@ export const versionRoutes = new Hono() // Get waveform data .get('/:id/waveform', async (c) => { const db = c.get('db'); + const userId = c.get('userId'); const versionId = c.req.param('id'); - const [version] = await db - .select() - .from(versions) - .where(eq(versions.id, versionId)) - .limit(1); + const [version] = await db.select().from(versions).where(eq(versions.id, versionId)).limit(1); + if (!version || !version.waveformDataKey) return c.json({ error: 'Not found' }, 404); - if (!version || !version.waveformDataKey) { - return c.json({ error: 'Not found' }, 404); - } + const [track] = await db.select().from(tracks).where(eq(tracks.id, version.trackId)).limit(1); + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId))) + .limit(1); + if (!membership) return c.json({ error: 'Not found' }, 404); const url = await createDownloadUrl(version.waveformDataKey); return c.json({ url }); diff --git a/apps/web/src/lib/components/audio/StemUploadDropzone.svelte b/apps/web/src/lib/components/audio/StemUploadDropzone.svelte index 0553fe5..d20afe3 100644 --- a/apps/web/src/lib/components/audio/StemUploadDropzone.svelte +++ b/apps/web/src/lib/components/audio/StemUploadDropzone.svelte @@ -124,7 +124,7 @@ accept="audio/*" multiple onchange={handleFileSelect} - hidden + style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;" />