feat: secure KYC storage, Google OAuth, terms gating

This commit is contained in:
arifal
2026-04-28 23:10:21 +07:00
parent 58da4608ac
commit 05d0929f7a
41 changed files with 3087 additions and 262 deletions
+63
View File
@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { organizerRepo } from "@/server/repositories/organizer.repo";
import {
isKycKind,
mimeFromKey,
readDecrypted,
} from "@/lib/secure-storage";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
interface RouteCtx {
params: Promise<{ id: string; kind: string }>;
}
export async function GET(_req: NextRequest, ctx: RouteCtx) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id, kind } = await ctx.params;
if (!isKycKind(kind)) {
return NextResponse.json({ error: "Kind tidak valid" }, { status: 400 });
}
const verification = await organizerRepo.findById(id);
if (!verification) {
return NextResponse.json({ error: "Tidak ditemukan" }, { status: 404 });
}
const isOwner = verification.userId === session.user.id;
const isAdmin = isAdminEmail(session.user.email);
if (!isOwner && !isAdmin) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const key = kind === "ktp" ? verification.ktpImageKey : verification.selfieKey;
if (!key) {
return NextResponse.json({ error: "File belum diunggah" }, { status: 404 });
}
let plain: Buffer;
try {
plain = await readDecrypted(kind, key);
} catch {
return NextResponse.json({ error: "File tidak dapat dibuka" }, { status: 500 });
}
return new NextResponse(new Uint8Array(plain), {
status: 200,
headers: {
"Content-Type": mimeFromKey(key),
"Content-Length": String(plain.length),
"Cache-Control": "private, no-store",
"X-Content-Type-Options": "nosniff",
"Content-Disposition": `inline; filename="${kind}-${id}"`,
},
});
}