feat: secure KYC storage, Google OAuth, terms gating
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user