diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3b2383f..3b94029 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -48,8 +48,13 @@ model AudioUpload { uploadError String? moderationStatus String? // "Pending" | "Reviewing" | "Approved" | "Rejected" moderationReason String? + // Discord ID of the signed-in owner. Existing rows without an owner are + // deliberately not shown to anyone, preventing legacy shared history. + ownerId String? accountId String? account RobloxAccount? @relation(fields: [accountId], references: [id]) + + @@index([ownerId, createdAt]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/app/api/history/route.ts b/src/app/api/history/route.ts index 44ad690..a35d270 100644 --- a/src/app/api/history/route.ts +++ b/src/app/api/history/route.ts @@ -1,16 +1,28 @@ import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/db"; +import { getAuthenticatedUserId } from "@/lib/auth"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -/** GET /api/history — list all conversion/upload history (newest first) */ +async function requireUser() { + const ownerId = await getAuthenticatedUserId(); + if (!ownerId) { + return { error: NextResponse.json({ ok: false, error: "Silakan login dengan Discord terlebih dahulu." }, { status: 401 }) }; + } + return { ownerId }; +} + +/** GET /api/history — only the current Discord user's history, newest first. */ export async function GET() { + const auth = await requireUser(); + if ("error" in auth) return auth.error; + try { const items = await db.audioUpload.findMany({ + where: { ownerId: auth.ownerId }, orderBy: { createdAt: "desc" }, take: 100, - include: { account: true }, }); return NextResponse.json({ ok: true, items }); } catch (e) { @@ -18,12 +30,16 @@ export async function GET() { } } -/** POST /api/history — save a conversion/upload record */ +/** POST /api/history — create a record owned by the current Discord user. */ export async function POST(req: NextRequest) { + const auth = await requireUser(); + if ("error" in auth) return auth.error; + try { const body = await req.json(); const created = await db.audioUpload.create({ data: { + ownerId: auth.ownerId, sourceType: body.sourceType || "file", sourceUrl: body.sourceUrl || null, sourceTitle: body.sourceTitle || null, @@ -44,7 +60,8 @@ export async function POST(req: NextRequest) { uploadError: body.uploadError || null, moderationStatus: body.moderationStatus || null, moderationReason: body.moderationReason || null, - accountId: body.accountId || null, + // Never accept ownerId from the browser. accountId is intentionally + // omitted: Roblox accounts are browser-session credentials, not shared DB rows. }, }); return NextResponse.json({ ok: true, item: created }); @@ -53,35 +70,43 @@ export async function POST(req: NextRequest) { } } -/** PATCH /api/history?id=xxx — update moderation status of a record */ +/** PATCH /api/history?id=xxx — update only a record owned by this user. */ export async function PATCH(req: NextRequest) { + const auth = await requireUser(); + if ("error" in auth) return auth.error; + try { - const url = new URL(req.url); - const id = url.searchParams.get("id"); + const id = new URL(req.url).searchParams.get("id"); if (!id) return NextResponse.json({ ok: false, error: "id wajib diisi" }, { status: 400 }); const body = await req.json(); - const updated = await db.audioUpload.update({ - where: { id }, + const result = await db.audioUpload.updateMany({ + where: { id, ownerId: auth.ownerId }, data: { moderationStatus: body.moderationStatus || null, moderationReason: body.moderationReason || null, uploadStatus: body.uploadStatus || undefined, + uploadError: body.uploadError || undefined, robloxAssetId: body.robloxAssetId || undefined, }, }); - return NextResponse.json({ ok: true, item: updated }); + if (!result.count) return NextResponse.json({ ok: false, error: "Riwayat tidak ditemukan." }, { status: 404 }); + const item = await db.audioUpload.findFirst({ where: { id, ownerId: auth.ownerId } }); + return NextResponse.json({ ok: true, item }); } catch (e) { return NextResponse.json({ ok: false, error: (e as Error).message }, { status: 500 }); } } -/** DELETE /api/history?id=xxx — delete a record */ +/** DELETE /api/history?id=xxx — delete only a record owned by this user. */ export async function DELETE(req: NextRequest) { + const auth = await requireUser(); + if ("error" in auth) return auth.error; + try { - const url = new URL(req.url); - const id = url.searchParams.get("id"); + const id = new URL(req.url).searchParams.get("id"); if (!id) return NextResponse.json({ ok: false, error: "id wajib diisi" }, { status: 400 }); - await db.audioUpload.delete({ where: { id } }); + const result = await db.audioUpload.deleteMany({ where: { id, ownerId: auth.ownerId } }); + if (!result.count) return NextResponse.json({ ok: false, error: "Riwayat tidak ditemukan." }, { status: 404 }); return NextResponse.json({ ok: true }); } catch (e) { return NextResponse.json({ ok: false, error: (e as Error).message }, { status: 500 }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 593ba68..11bec34 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,6 +10,7 @@ import { HistoryList } from "@/components/history-list"; import { PreviewList } from "@/components/preview-list"; import { ThemeToggle } from "@/components/theme-toggle"; import { UserMenu } from "@/components/user-menu"; +import { SessionWorkspace } from "@/components/session-workspace"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; @@ -21,6 +22,7 @@ export default function Home() { return (
{item.assetName}
{isUploaded && !item.moderationStatus && ( -