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
@@ -0,0 +1,27 @@
/*
Warnings:
- You are about to drop the column `ktpImageUrl` on the `OrganizerVerification` table. All the data in the column will be lost.
- You are about to drop the column `nik` on the `OrganizerVerification` table. All the data in the column will be lost.
- You are about to drop the column `selfieUrl` on the `OrganizerVerification` table. All the data in the column will be lost.
- A unique constraint covering the columns `[nikHash]` on the table `OrganizerVerification` will be added. If there are existing duplicate values, this will fail.
- Added the required column `ktpImageKey` to the `OrganizerVerification` table without a default value. This is not possible if the table is not empty.
- Added the required column `nikEncrypted` to the `OrganizerVerification` table without a default value. This is not possible if the table is not empty.
- Added the required column `nikHash` to the `OrganizerVerification` table without a default value. This is not possible if the table is not empty.
- Added the required column `selfieKey` to the `OrganizerVerification` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "OrganizerVerification_nik_key";
-- AlterTable
ALTER TABLE "OrganizerVerification" DROP COLUMN "ktpImageUrl",
DROP COLUMN "nik",
DROP COLUMN "selfieUrl",
ADD COLUMN "ktpImageKey" TEXT NOT NULL,
ADD COLUMN "nikEncrypted" TEXT NOT NULL,
ADD COLUMN "nikHash" TEXT NOT NULL,
ADD COLUMN "selfieKey" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "OrganizerVerification_nikHash_key" ON "OrganizerVerification"("nikHash");
@@ -0,0 +1,26 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL;
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+32 -7
View File
@@ -11,7 +11,8 @@ model User {
id String @id @default(cuid())
name String
email String @unique
password String
/// Hash bcrypt. Null untuk user yang sign-in via OAuth (mis. Google).
password String?
image String?
/// Apakah user telah menyetujui Syarat & Ketentuan dan Kebijakan Privasi
acceptedTermsAndPrivacy Boolean @default(false)
@@ -20,6 +21,7 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
trips Trip[]
participations TripParticipant[]
tripReviews TripReview[]
@@ -28,6 +30,27 @@ model User {
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
}
/// Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
/// Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model OrganizerVerification {
id String @id @default(cuid())
userId String @unique
@@ -35,15 +58,17 @@ model OrganizerVerification {
/// Nama lengkap sesuai KTP
fullName String
/// Nomor Induk Kependudukan (PII — perlakukan sensitif)
nik String @unique
/// NIK terenkripsi (AES-256-GCM, base64). Plaintext tidak disimpan.
nikEncrypted String
/// HMAC-SHA256(NIK + pepper) untuk uniqueness lookup tanpa membuka plaintext.
nikHash String @unique
birthDate DateTime
address String
/// URL foto KTP (untuk MVP pakai hosting; pindah ke storage privat untuk produksi)
ktpImageUrl String
/// URL selfie memegang KTP
selfieUrl String
/// Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public.
ktpImageKey String
/// Storage key selfie memegang KTP.
selfieKey String
bankName String
bankAccountNumber String
+11 -6
View File
@@ -2,6 +2,7 @@ import "dotenv/config";
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import bcrypt from "bcryptjs";
import { encryptString, hmacHex } from "../lib/crypto";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
@@ -97,16 +98,19 @@ async function main() {
// ==================== ORGANIZER VERIFICATIONS ====================
const verifiedAt = new Date();
const dedeNik = "3201010101010001";
const panjiNik = "3201010101010002";
await prisma.organizerVerification.createMany({
data: [
{
userId: dede.id,
fullName: "Dede Inoen",
nik: "3201010101010001",
nikEncrypted: encryptString(dedeNik),
nikHash: hmacHex(dedeNik),
birthDate: new Date(Date.UTC(1990, 0, 1)),
address: "Jl. Pendaki No. 1, Garut, Jawa Barat",
ktpImageUrl: "https://placehold.co/600x400/png?text=KTP+Dede",
selfieUrl: "https://placehold.co/600x400/png?text=Selfie+Dede",
ktpImageKey: "ktp/seed-dede.jpg",
selfieKey: "selfie/seed-dede.jpg",
bankName: "BCA",
bankAccountNumber: "1234567890",
bankAccountName: "Dede Inoen",
@@ -117,11 +121,12 @@ async function main() {
{
userId: panji.id,
fullName: "Panji Petualang",
nik: "3201010101010002",
nikEncrypted: encryptString(panjiNik),
nikHash: hmacHex(panjiNik),
birthDate: new Date(Date.UTC(1985, 5, 15)),
address: "Jl. Adventure No. 7, Kuningan, Jawa Barat",
ktpImageUrl: "https://placehold.co/600x400/png?text=KTP+Panji",
selfieUrl: "https://placehold.co/600x400/png?text=Selfie+Panji",
ktpImageKey: "ktp/seed-panji.jpg",
selfieKey: "selfie/seed-panji.jpg",
bankName: "Mandiri",
bankAccountNumber: "9876543210",
bankAccountName: "Panji Petualang",