change selfie with ktp to selfie with setrip tag

This commit is contained in:
2026-05-08 20:02:11 +07:00
parent f5d86d2414
commit ccb3437e82
24 changed files with 127 additions and 84 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"WebFetch(domain:unsplash.com)" "WebFetch(domain:unsplash.com)",
"Bash(npx prisma *)"
] ]
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
# private uploads (KYC: KTP / selfie). Never serve directly. # private uploads (KYC: KTP / liveness). Never serve directly.
/uploads/ /uploads/
# vercel # vercel
+1 -1
View File
@@ -236,7 +236,7 @@ Alur data mengikuti pola yang sama: **UI (`app/`) → server actions (`features/
### Verifikasi organizer (KYC ringan) ### Verifikasi organizer (KYC ringan)
- Model `OrganizerVerification` (1-1 ke `User`) menyimpan KTP (nama, NIK unik, tanggal lahir, alamat), URL foto KTP & selfie, data rekening bank, dan status `PENDING` / `APPROVED` / `REJECTED` + audit reviewer. - Model `OrganizerVerification` (1-1 ke `User`) menyimpan KTP (nama, NIK unik, tanggal lahir, alamat), storage key foto KTP & foto liveness (user memegang kertas tulisan "SETRIP" sebagai bukti pengajuan), data rekening bank, dan status `PENDING` / `APPROVED` / `REJECTED` + audit reviewer.
- Alur: user submit di `/verify` (`features/organizer/`) → admin review di `/admin/verifications` → setujui/tolak. - Alur: user submit di `/verify` (`features/organizer/`) → admin review di `/admin/verifications` → setujui/tolak.
- **Gate trip berbayar:** `createTripAction` menolak `price > 0` jika user belum `APPROVED` (`organizerService.isApproved`). - **Gate trip berbayar:** `createTripAction` menolak `price > 0` jika user belum `APPROVED` (`organizerService.isApproved`).
- **Akses admin:** `lib/admin.ts → isAdminEmail()` membaca `ADMIN_EMAILS` (env, comma-separated). - **Akses admin:** `lib/admin.ts → isAdminEmail()` membaca `ADMIN_EMAILS` (env, comma-separated).
+2 -2
View File
@@ -23,7 +23,7 @@ Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tid
Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini. Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini.
**Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, selfie, dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip. **Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, foto liveness (memegang kertas tulisan "SETRIP"), dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip.
### 3. Peserta: mencari trip ### 3. Peserta: mencari trip
@@ -97,7 +97,7 @@ Alur ini menggambarkan satu peserta dari pertama kali mendaftar sampai pembayara
|--------|-------------| |--------|-------------|
| Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer | | Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer |
| Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual | | Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual |
| Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, selfie, rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. | | Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, foto liveness (memegang kertas "SETRIP"), rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. |
| Persetujuan T&C / Privasi | `User.acceptedTermsAndPrivacy` + `User.acceptedAt`, dicentang saat registrasi (link ke `/terms` & `/privacy`). | | Persetujuan T&C / Privasi | `User.acceptedTermsAndPrivacy` + `User.acceptedAt`, dicentang saat registrasi (link ke `/terms` & `/privacy`). |
## Menjalankan secara lokal ## Menjalankan secara lokal
+20
View File
@@ -42,6 +42,26 @@ Selesai. `tsc --noEmit` lulus. Migration `20260508130000_add_trip_vibe` belum di
--- ---
## Patch — KYC liveness photo rename (di luar fase social repositioning)
Mengubah foto liveness dari "selfie memegang KTP" (pola KYC standar) menjadi "memegang kertas tulisan SETRIP".
| Item | Status | File |
|---|---|---|
| Field `selfieKey``livenessKey` di `OrganizerVerification` | ✅ | [prisma/schema.prisma](prisma/schema.prisma), [prisma/migrations/20260508140000_rename_selfie_to_liveness/migration.sql](prisma/migrations/20260508140000_rename_selfie_to_liveness/migration.sql) |
| Storage kind `selfie``liveness` (path `liveness/<id>.<ext>`) | ✅ | [lib/secure-storage.ts](lib/secure-storage.ts) |
| Validasi + action + service + verify-form + review-card | ✅ | [features/organizer/schemas.ts](features/organizer/schemas.ts), [features/organizer/actions.ts](features/organizer/actions.ts), [server/services/organizer.service.ts](server/services/organizer.service.ts), [features/organizer/components/verify-form.tsx](features/organizer/components/verify-form.tsx), [features/organizer/components/review-card.tsx](features/organizer/components/review-card.tsx) |
| API routes `/api/upload/kyc` & `/api/files/kyc/[id]/[kind]` | ✅ | [app/api/upload/kyc/route.ts](app/api/upload/kyc/route.ts), [app/api/files/kyc/[id]/[kind]/route.ts](app/api/files/kyc/%5Bid%5D/%5Bkind%5D/route.ts) |
| Halaman verify, admin, seed, README, ARCHITECTURE | ✅ | [app/verify/page.tsx](app/verify/page.tsx), [app/admin/verifications/page.tsx](app/admin/verifications/page.tsx), [app/create-trip/page.tsx](app/create-trip/page.tsx), [prisma/seed.ts](prisma/seed.ts), [README.md](README.md), [ARCHITECTURE.md](ARCHITECTURE.md) |
**Trade-off keamanan yang sudah dikomunikasikan:** pola "selfie + KTP" lebih kuat (membuktikan KTP fisik di tangan pemilik). Pola "selfie + kertas SETRIP" lebih lemah dari sisi binding KTP↔orang, tapi mengurangi paparan KTP user dan masih mencegah replay dari platform lain. Risiko fraud naik sedikit — tetap dipilih atas request user.
**Catatan migrasi data lama:** kolom DB di-rename, tapi nilai-nilai key lama masih punya prefix `selfie/` (mis. `selfie/abc.jpg`). Setelah migration di-apply, validasi schema menolak prefix lama → user dengan pengajuan PENDING perlu re-upload foto liveness baru. Folder fisik `uploads/private/selfie/` tidak dipakai lagi, bisa dihapus manual setelah konfirmasi tidak ada data aktif yang merujuk.
**Tindakan manual:** jalankan `npx prisma migrate deploy` untuk apply `20260508140000_rename_selfie_to_liveness` (sekarang total 4 migration pending kalau belum pernah deploy).
---
## Phase C — Interaksi & continuity (separate, lebih besar) ⏳ ## Phase C — Interaksi & continuity (separate, lebih besar) ⏳
Belum mulai. Setiap item bisa jadi PR terpisah karena perlu schema baru + UI substansial. Belum mulai. Setiap item bisa jadi PR terpisah karena perlu schema baru + UI substansial.
+2 -1
View File
@@ -60,7 +60,8 @@ export default async function AdminVerificationsPage({ searchParams }: PageProps
Review Verifikasi Organizer Review Verifikasi Organizer
</h1> </h1>
<p className="mt-1 text-sm text-neutral-500"> <p className="mt-1 text-sm text-neutral-500">
Periksa data KTP, selfie, dan rekening sebelum menyetujui. Periksa data KTP, foto liveness (memegang kertas SETRIP), dan rekening
sebelum menyetujui.
</p> </p>
</header> </header>
+1 -1
View File
@@ -38,7 +38,7 @@ export async function GET(_req: NextRequest, ctx: RouteCtx) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 }); return NextResponse.json({ error: "Forbidden" }, { status: 403 });
} }
const key = kind === "ktp" ? verification.ktpImageKey : verification.selfieKey; const key = kind === "ktp" ? verification.ktpImageKey : verification.livenessKey;
if (!key) { if (!key) {
return NextResponse.json({ error: "File belum diunggah" }, { status: 404 }); return NextResponse.json({ error: "File belum diunggah" }, { status: 404 });
} }
+4 -1
View File
@@ -28,7 +28,10 @@ export async function POST(req: NextRequest) {
const file = form.get("file"); const file = form.get("file");
if (!isKycKind(kind)) { if (!isKycKind(kind)) {
return NextResponse.json({ error: "kind harus 'ktp' atau 'selfie'" }, { status: 400 }); return NextResponse.json(
{ error: "kind harus 'ktp' atau 'liveness'" },
{ status: 400 }
);
} }
if (!(file instanceof File)) { if (!(file instanceof File)) {
return NextResponse.json({ error: "File wajib diisi" }, { status: 400 }); return NextResponse.json({ error: "File wajib diisi" }, { status: 400 });
+1 -1
View File
@@ -79,7 +79,7 @@ function VerificationBanner({
<p className="mt-1 text-sm text-neutral-700"> <p className="mt-1 text-sm text-neutral-700">
{isRejected {isRejected
? "Pengajuan sebelumnya ditolak. Untuk membuat trip berbayar, perbaiki data dan ajukan ulang." ? "Pengajuan sebelumnya ditolak. Untuk membuat trip berbayar, perbaiki data dan ajukan ulang."
: "Untuk membuat trip berbayar, akun kamu perlu diverifikasi (KTP, selfie, & rekening). Trip gratis tidak butuh verifikasi."} : "Untuk membuat trip berbayar, akun kamu perlu diverifikasi (KTP, foto memegang kertas SETRIP, & rekening). Trip gratis tidak butuh verifikasi."}
</p> </p>
</div> </div>
<Link <Link
File diff suppressed because one or more lines are too long
@@ -1100,7 +1100,7 @@ export const OrganizerVerificationScalarFieldEnum = {
birthDate: 'birthDate', birthDate: 'birthDate',
address: 'address', address: 'address',
ktpImageKey: 'ktpImageKey', ktpImageKey: 'ktpImageKey',
selfieKey: 'selfieKey', livenessKey: 'livenessKey',
bankName: 'bankName', bankName: 'bankName',
bankAccountNumber: 'bankAccountNumber', bankAccountNumber: 'bankAccountNumber',
bankAccountName: 'bankAccountName', bankAccountName: 'bankAccountName',
@@ -135,7 +135,7 @@ export const OrganizerVerificationScalarFieldEnum = {
birthDate: 'birthDate', birthDate: 'birthDate',
address: 'address', address: 'address',
ktpImageKey: 'ktpImageKey', ktpImageKey: 'ktpImageKey',
selfieKey: 'selfieKey', livenessKey: 'livenessKey',
bankName: 'bankName', bankName: 'bankName',
bankAccountNumber: 'bankAccountNumber', bankAccountNumber: 'bankAccountNumber',
bankAccountName: 'bankAccountName', bankAccountName: 'bankAccountName',
@@ -33,7 +33,7 @@ export type OrganizerVerificationMinAggregateOutputType = {
birthDate: Date | null birthDate: Date | null
address: string | null address: string | null
ktpImageKey: string | null ktpImageKey: string | null
selfieKey: string | null livenessKey: string | null
bankName: string | null bankName: string | null
bankAccountNumber: string | null bankAccountNumber: string | null
bankAccountName: string | null bankAccountName: string | null
@@ -55,7 +55,7 @@ export type OrganizerVerificationMaxAggregateOutputType = {
birthDate: Date | null birthDate: Date | null
address: string | null address: string | null
ktpImageKey: string | null ktpImageKey: string | null
selfieKey: string | null livenessKey: string | null
bankName: string | null bankName: string | null
bankAccountNumber: string | null bankAccountNumber: string | null
bankAccountName: string | null bankAccountName: string | null
@@ -77,7 +77,7 @@ export type OrganizerVerificationCountAggregateOutputType = {
birthDate: number birthDate: number
address: number address: number
ktpImageKey: number ktpImageKey: number
selfieKey: number livenessKey: number
bankName: number bankName: number
bankAccountNumber: number bankAccountNumber: number
bankAccountName: number bankAccountName: number
@@ -101,7 +101,7 @@ export type OrganizerVerificationMinAggregateInputType = {
birthDate?: true birthDate?: true
address?: true address?: true
ktpImageKey?: true ktpImageKey?: true
selfieKey?: true livenessKey?: true
bankName?: true bankName?: true
bankAccountNumber?: true bankAccountNumber?: true
bankAccountName?: true bankAccountName?: true
@@ -123,7 +123,7 @@ export type OrganizerVerificationMaxAggregateInputType = {
birthDate?: true birthDate?: true
address?: true address?: true
ktpImageKey?: true ktpImageKey?: true
selfieKey?: true livenessKey?: true
bankName?: true bankName?: true
bankAccountNumber?: true bankAccountNumber?: true
bankAccountName?: true bankAccountName?: true
@@ -145,7 +145,7 @@ export type OrganizerVerificationCountAggregateInputType = {
birthDate?: true birthDate?: true
address?: true address?: true
ktpImageKey?: true ktpImageKey?: true
selfieKey?: true livenessKey?: true
bankName?: true bankName?: true
bankAccountNumber?: true bankAccountNumber?: true
bankAccountName?: true bankAccountName?: true
@@ -240,7 +240,7 @@ export type OrganizerVerificationGroupByOutputType = {
birthDate: Date birthDate: Date
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -283,7 +283,7 @@ export type OrganizerVerificationWhereInput = {
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -307,7 +307,7 @@ export type OrganizerVerificationOrderByWithRelationInput = {
birthDate?: Prisma.SortOrder birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder address?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder livenessKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder bankAccountName?: Prisma.SortOrder
@@ -334,7 +334,7 @@ export type OrganizerVerificationWhereUniqueInput = Prisma.AtLeast<{
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -358,7 +358,7 @@ export type OrganizerVerificationOrderByWithAggregationInput = {
birthDate?: Prisma.SortOrder birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder address?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder livenessKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder bankAccountName?: Prisma.SortOrder
@@ -386,7 +386,7 @@ export type OrganizerVerificationScalarWhereWithAggregatesInput = {
birthDate?: Prisma.DateTimeWithAggregatesFilter<"OrganizerVerification"> | Date | string birthDate?: Prisma.DateTimeWithAggregatesFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string address?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string ktpImageKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string livenessKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string bankName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string bankAccountNumber?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string bankAccountName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
@@ -407,7 +407,7 @@ export type OrganizerVerificationCreateInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -430,7 +430,7 @@ export type OrganizerVerificationUncheckedCreateInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -451,7 +451,7 @@ export type OrganizerVerificationUpdateInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -474,7 +474,7 @@ export type OrganizerVerificationUncheckedUpdateInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -496,7 +496,7 @@ export type OrganizerVerificationCreateManyInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -517,7 +517,7 @@ export type OrganizerVerificationUpdateManyMutationInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -538,7 +538,7 @@ export type OrganizerVerificationUncheckedUpdateManyInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -575,7 +575,7 @@ export type OrganizerVerificationCountOrderByAggregateInput = {
birthDate?: Prisma.SortOrder birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder address?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder livenessKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder bankAccountName?: Prisma.SortOrder
@@ -597,7 +597,7 @@ export type OrganizerVerificationMaxOrderByAggregateInput = {
birthDate?: Prisma.SortOrder birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder address?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder livenessKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder bankAccountName?: Prisma.SortOrder
@@ -619,7 +619,7 @@ export type OrganizerVerificationMinOrderByAggregateInput = {
birthDate?: Prisma.SortOrder birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder address?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder livenessKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder bankAccountName?: Prisma.SortOrder
@@ -718,7 +718,7 @@ export type OrganizerVerificationCreateWithoutUserInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -739,7 +739,7 @@ export type OrganizerVerificationUncheckedCreateWithoutUserInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -765,7 +765,7 @@ export type OrganizerVerificationCreateWithoutReviewedByInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -787,7 +787,7 @@ export type OrganizerVerificationUncheckedCreateWithoutReviewedByInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -828,7 +828,7 @@ export type OrganizerVerificationUpdateWithoutUserInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -849,7 +849,7 @@ export type OrganizerVerificationUncheckedUpdateWithoutUserInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -890,7 +890,7 @@ export type OrganizerVerificationScalarWhereInput = {
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string livenessKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -912,7 +912,7 @@ export type OrganizerVerificationCreateManyReviewedByInput = {
birthDate: Date | string birthDate: Date | string
address: string address: string
ktpImageKey: string ktpImageKey: string
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -932,7 +932,7 @@ export type OrganizerVerificationUpdateWithoutReviewedByInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -954,7 +954,7 @@ export type OrganizerVerificationUncheckedUpdateWithoutReviewedByInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -975,7 +975,7 @@ export type OrganizerVerificationUncheckedUpdateManyWithoutReviewedByInput = {
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string livenessKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -998,7 +998,7 @@ export type OrganizerVerificationSelect<ExtArgs extends runtime.Types.Extensions
birthDate?: boolean birthDate?: boolean
address?: boolean address?: boolean
ktpImageKey?: boolean ktpImageKey?: boolean
selfieKey?: boolean livenessKey?: boolean
bankName?: boolean bankName?: boolean
bankAccountNumber?: boolean bankAccountNumber?: boolean
bankAccountName?: boolean bankAccountName?: boolean
@@ -1022,7 +1022,7 @@ export type OrganizerVerificationSelectCreateManyAndReturn<ExtArgs extends runti
birthDate?: boolean birthDate?: boolean
address?: boolean address?: boolean
ktpImageKey?: boolean ktpImageKey?: boolean
selfieKey?: boolean livenessKey?: boolean
bankName?: boolean bankName?: boolean
bankAccountNumber?: boolean bankAccountNumber?: boolean
bankAccountName?: boolean bankAccountName?: boolean
@@ -1046,7 +1046,7 @@ export type OrganizerVerificationSelectUpdateManyAndReturn<ExtArgs extends runti
birthDate?: boolean birthDate?: boolean
address?: boolean address?: boolean
ktpImageKey?: boolean ktpImageKey?: boolean
selfieKey?: boolean livenessKey?: boolean
bankName?: boolean bankName?: boolean
bankAccountNumber?: boolean bankAccountNumber?: boolean
bankAccountName?: boolean bankAccountName?: boolean
@@ -1070,7 +1070,7 @@ export type OrganizerVerificationSelectScalar = {
birthDate?: boolean birthDate?: boolean
address?: boolean address?: boolean
ktpImageKey?: boolean ktpImageKey?: boolean
selfieKey?: boolean livenessKey?: boolean
bankName?: boolean bankName?: boolean
bankAccountNumber?: boolean bankAccountNumber?: boolean
bankAccountName?: boolean bankAccountName?: boolean
@@ -1083,7 +1083,7 @@ export type OrganizerVerificationSelectScalar = {
updatedAt?: boolean updatedAt?: boolean
} }
export type OrganizerVerificationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "fullName" | "nikEncrypted" | "nikHash" | "birthDate" | "address" | "ktpImageKey" | "selfieKey" | "bankName" | "bankAccountNumber" | "bankAccountName" | "status" | "rejectionReason" | "reviewedAt" | "reviewedById" | "verifiedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["organizerVerification"]> export type OrganizerVerificationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "fullName" | "nikEncrypted" | "nikHash" | "birthDate" | "address" | "ktpImageKey" | "livenessKey" | "bankName" | "bankAccountNumber" | "bankAccountName" | "status" | "rejectionReason" | "reviewedAt" | "reviewedById" | "verifiedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["organizerVerification"]>
export type OrganizerVerificationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type OrganizerVerificationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs> user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
reviewedBy?: boolean | Prisma.OrganizerVerification$reviewedByArgs<ExtArgs> reviewedBy?: boolean | Prisma.OrganizerVerification$reviewedByArgs<ExtArgs>
@@ -1125,9 +1125,10 @@ export type $OrganizerVerificationPayload<ExtArgs extends runtime.Types.Extensio
*/ */
ktpImageKey: string ktpImageKey: string
/** /**
* Storage key selfie memegang KTP. * Storage key foto liveness — user memegang kertas bertuliskan "SETRIP".
* (Sebelumnya: selfie memegang KTP. Diganti supaya user tidak perlu memajang KTP dua kali.)
*/ */
selfieKey: string livenessKey: string
bankName: string bankName: string
bankAccountNumber: string bankAccountNumber: string
bankAccountName: string bankAccountName: string
@@ -1571,7 +1572,7 @@ export interface OrganizerVerificationFieldRefs {
readonly birthDate: Prisma.FieldRef<"OrganizerVerification", 'DateTime'> readonly birthDate: Prisma.FieldRef<"OrganizerVerification", 'DateTime'>
readonly address: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly address: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly ktpImageKey: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly ktpImageKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly selfieKey: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly livenessKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankName: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly bankName: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankAccountNumber: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly bankAccountNumber: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankAccountName: Prisma.FieldRef<"OrganizerVerification", 'String'> readonly bankAccountName: Prisma.FieldRef<"OrganizerVerification", 'String'>
+1 -1
View File
@@ -21,7 +21,7 @@ export default async function VerifyPage() {
birthDate: verification.birthDate, birthDate: verification.birthDate,
address: verification.address, address: verification.address,
ktpImageKey: verification.ktpImageKey, ktpImageKey: verification.ktpImageKey,
selfieKey: verification.selfieKey, livenessKey: verification.livenessKey,
bankName: verification.bankName, bankName: verification.bankName,
bankAccountNumber: verification.bankAccountNumber, bankAccountNumber: verification.bankAccountNumber,
bankAccountName: verification.bankAccountName, bankAccountName: verification.bankAccountName,
+1 -1
View File
@@ -4,7 +4,7 @@ NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_SITE_URL="https://arifal.imola.ai" NEXT_PUBLIC_SITE_URL="https://arifal.imola.ai"
ADMIN_EMAILS=admin@setrip.id ADMIN_EMAILS=admin@setrip.id
# 32-byte key (hex) for AES-256-GCM encryption of KYC data (NIK + KTP/selfie files) # 32-byte key (hex) for AES-256-GCM encryption of KYC data (NIK + KTP/liveness files)
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
KYC_ENCRYPTION_KEY= KYC_ENCRYPTION_KEY=
# 32-byte hex secret used as HMAC pepper for NIK uniqueness lookup # 32-byte hex secret used as HMAC pepper for NIK uniqueness lookup
+1 -1
View File
@@ -19,7 +19,7 @@ export async function submitVerificationAction(formData: FormData) {
birthDate: formData.get("birthDate") as string, birthDate: formData.get("birthDate") as string,
address: formData.get("address") as string, address: formData.get("address") as string,
ktpImageKey: formData.get("ktpImageKey") as string, ktpImageKey: formData.get("ktpImageKey") as string,
selfieKey: formData.get("selfieKey") as string, livenessKey: formData.get("livenessKey") as string,
bankName: formData.get("bankName") as string, bankName: formData.get("bankName") as string,
bankAccountNumber: formData.get("bankAccountNumber") as string, bankAccountNumber: formData.get("bankAccountNumber") as string,
bankAccountName: formData.get("bankAccountName") as string, bankAccountName: formData.get("bankAccountName") as string,
@@ -93,8 +93,8 @@ export function ReviewCard({ verification }: { verification: Verification }) {
src={`/api/files/kyc/${verification.id}/ktp`} src={`/api/files/kyc/${verification.id}/ktp`}
/> />
<ImagePreview <ImagePreview
label="Selfie + KTP" label="Foto memegang kertas SETRIP"
src={`/api/files/kyc/${verification.id}/selfie`} src={`/api/files/kyc/${verification.id}/liveness`}
/> />
</div> </div>
+22 -13
View File
@@ -10,13 +10,13 @@ type Initial = {
birthDate: Date; birthDate: Date;
address: string; address: string;
ktpImageKey: string; ktpImageKey: string;
selfieKey: string; livenessKey: string;
bankName: string; bankName: string;
bankAccountNumber: string; bankAccountNumber: string;
bankAccountName: string; bankAccountName: string;
} | null; } | null;
type UploadKind = "ktp" | "selfie"; type UploadKind = "ktp" | "liveness";
const ACCEPT_MIME = "image/jpeg,image/png,image/webp"; const ACCEPT_MIME = "image/jpeg,image/png,image/webp";
const MAX_BYTES = 5 * 1024 * 1024; const MAX_BYTES = 5 * 1024 * 1024;
@@ -33,19 +33,19 @@ export function VerifyForm({ initial }: { initial: Initial }) {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [ktpKey, setKtpKey] = useState(initial?.ktpImageKey ?? ""); const [ktpKey, setKtpKey] = useState(initial?.ktpImageKey ?? "");
const [selfieKey, setSelfieKey] = useState(initial?.selfieKey ?? ""); const [livenessKey, setLivenessKey] = useState(initial?.livenessKey ?? "");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault(); e.preventDefault();
setError(""); setError("");
if (!ktpKey || !selfieKey) { if (!ktpKey || !livenessKey) {
setError("Foto KTP dan selfie wajib diunggah"); setError("Foto KTP dan foto memegang kertas SETRIP wajib diunggah");
return; return;
} }
setLoading(true); setLoading(true);
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
formData.set("ktpImageKey", ktpKey); formData.set("ktpImageKey", ktpKey);
formData.set("selfieKey", selfieKey); formData.set("livenessKey", livenessKey);
const result = await submitVerificationAction(formData); const result = await submitVerificationAction(formData);
setLoading(false); setLoading(false);
if (result.error) { if (result.error) {
@@ -143,13 +143,22 @@ export function VerifyForm({ initial }: { initial: Initial }) {
onChange={setKtpKey} onChange={setKtpKey}
onError={setError} onError={setError}
/> />
<FileUpload <div>
label="Selfie dengan KTP" <FileUpload
kind="selfie" label="Foto kamu memegang kertas tulisan SETRIP"
value={selfieKey} kind="liveness"
onChange={setSelfieKey} value={livenessKey}
onError={setError} onChange={setLivenessKey}
/> onError={setError}
/>
<p className="mt-1.5 text-[11px] leading-relaxed text-neutral-500">
Tulis kata <span className="font-semibold">SETRIP</span> dengan
tangan di selembar kertas, lalu foto diri kamu sambil memegang
kertas itu pastikan wajah & tulisan terlihat jelas dalam satu
foto. Foto ini bukti bahwa pengajuan benar dilakukan oleh kamu
sendiri.
</p>
</div>
</div> </div>
</section> </section>
+5 -2
View File
@@ -26,10 +26,13 @@ export const submitVerificationSchema = z.object({
.string() .string()
.trim() .trim()
.regex(/^ktp\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto KTP wajib diunggah"), .regex(/^ktp\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto KTP wajib diunggah"),
selfieKey: z livenessKey: z
.string() .string()
.trim() .trim()
.regex(/^selfie\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto selfie wajib diunggah"), .regex(
/^liveness\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/,
"Foto memegang kertas SETRIP wajib diunggah"
),
bankName: z bankName: z
.string() .string()
.trim() .trim()
+3 -3
View File
@@ -3,11 +3,11 @@ import path from "node:path";
import crypto from "node:crypto"; import crypto from "node:crypto";
import { encryptBuffer, decryptBuffer } from "@/lib/crypto"; import { encryptBuffer, decryptBuffer } from "@/lib/crypto";
export type KycKind = "ktp" | "selfie"; export type KycKind = "ktp" | "liveness";
const KIND_DIRS: Record<KycKind, string> = { const KIND_DIRS: Record<KycKind, string> = {
ktp: "ktp", ktp: "ktp",
selfie: "selfie", liveness: "liveness",
}; };
/** Bytes. ~5MB matches the form limit; raise here if you change the upload route. */ /** Bytes. ~5MB matches the form limit; raise here if you change the upload route. */
@@ -43,7 +43,7 @@ export type StoredFileMeta = {
}; };
export function isKycKind(value: string): value is KycKind { export function isKycKind(value: string): value is KycKind {
return value === "ktp" || value === "selfie"; return value === "ktp" || value === "liveness";
} }
/** Resolve a storage key (`ktp/abc.jpg`) to an absolute path inside the upload dir. Throws on traversal. */ /** Resolve a storage key (`ktp/abc.jpg`) to an absolute path inside the upload dir. Throws on traversal. */
@@ -0,0 +1,4 @@
-- AlterTable: rename selfieKey -> livenessKey.
-- Sebelumnya: storage key untuk selfie memegang KTP.
-- Sekarang: storage key foto liveness (user memegang kertas tulisan "SETRIP").
ALTER TABLE "OrganizerVerification" RENAME COLUMN "selfieKey" TO "livenessKey";
+3 -2
View File
@@ -99,8 +99,9 @@ model OrganizerVerification {
/// Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public. /// Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public.
ktpImageKey String ktpImageKey String
/// Storage key selfie memegang KTP. /// Storage key foto liveness — user memegang kertas bertuliskan "SETRIP".
selfieKey String /// (Sebelumnya: selfie memegang KTP. Diganti supaya user tidak perlu memajang KTP dua kali.)
livenessKey String
bankName String bankName String
bankAccountNumber String bankAccountNumber String
+2 -2
View File
@@ -69,7 +69,7 @@ async function main() {
birthDate: new Date(Date.UTC(1990, 0, 1)), birthDate: new Date(Date.UTC(1990, 0, 1)),
address: "Jl. Pendaki No. 1, Garut, Jawa Barat", address: "Jl. Pendaki No. 1, Garut, Jawa Barat",
ktpImageKey: "ktp/seed-dede.jpg", ktpImageKey: "ktp/seed-dede.jpg",
selfieKey: "selfie/seed-dede.jpg", livenessKey: "liveness/seed-dede.jpg",
bankName: "BCA", bankName: "BCA",
bankAccountNumber: "1234567890", bankAccountNumber: "1234567890",
bankAccountName: "Dede Inoen", bankAccountName: "Dede Inoen",
@@ -85,7 +85,7 @@ async function main() {
birthDate: new Date(Date.UTC(1985, 5, 15)), birthDate: new Date(Date.UTC(1985, 5, 15)),
address: "Jl. Adventure No. 7, Kuningan, Jawa Barat", address: "Jl. Adventure No. 7, Kuningan, Jawa Barat",
ktpImageKey: "ktp/seed-panji.jpg", ktpImageKey: "ktp/seed-panji.jpg",
selfieKey: "selfie/seed-panji.jpg", livenessKey: "liveness/seed-panji.jpg",
bankName: "Mandiri", bankName: "Mandiri",
bankAccountNumber: "9876543210", bankAccountNumber: "9876543210",
bankAccountName: "Panji Petualang", bankAccountName: "Panji Petualang",
+2 -2
View File
@@ -7,7 +7,7 @@ type SubmitInput = {
birthDate: Date; birthDate: Date;
address: string; address: string;
ktpImageKey: string; ktpImageKey: string;
selfieKey: string; livenessKey: string;
bankName: string; bankName: string;
bankAccountNumber: string; bankAccountNumber: string;
bankAccountName: string; bankAccountName: string;
@@ -36,7 +36,7 @@ export const organizerService = {
birthDate: data.birthDate, birthDate: data.birthDate,
address: data.address, address: data.address,
ktpImageKey: data.ktpImageKey, ktpImageKey: data.ktpImageKey,
selfieKey: data.selfieKey, livenessKey: data.livenessKey,
bankName: data.bankName, bankName: data.bankName,
bankAccountNumber: data.bankAccountNumber, bankAccountNumber: data.bankAccountNumber,
bankAccountName: data.bankAccountName, bankAccountName: data.bankAccountName,