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
+3
View File
@@ -33,6 +33,9 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
# private uploads (KYC: KTP / selfie). Never serve directly.
/uploads/
# vercel
.vercel
+89
View File
@@ -0,0 +1,89 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
import { acceptTermsAction } from "@/features/auth/actions";
export function AcceptTermsForm() {
const router = useRouter();
const { update } = useSession();
const [checked, setChecked] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleAccept() {
setError("");
setLoading(true);
const result = await acceptTermsAction();
if (result.error) {
setError(result.error);
setLoading(false);
return;
}
// Refresh JWT supaya middleware lihat acceptedTermsAndPrivacy=true
await update();
router.replace("/");
router.refresh();
}
return (
<div className="mt-6 rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
{error}
</div>
)}
<label className="flex items-start gap-2.5 text-sm text-neutral-700">
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
className="mt-0.5 h-4 w-4 shrink-0 rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
/>
<span>
Saya telah membaca dan menyetujui{" "}
<Link
href="/terms"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-primary-600 hover:text-primary-700"
>
Syarat &amp; Ketentuan
</Link>{" "}
dan{" "}
<Link
href="/privacy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold text-primary-600 hover:text-primary-700"
>
Kebijakan Privasi
</Link>{" "}
SeTrip.
</span>
</label>
<div className="mt-5 flex flex-col gap-2 sm:flex-row">
<button
type="button"
onClick={handleAccept}
disabled={!checked || loading}
className="flex-1 rounded-xl bg-primary-600 py-2.5 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Memproses..." : "Setuju & Lanjutkan"}
</button>
<button
type="button"
onClick={() => signOut({ callbackUrl: "/login" })}
disabled={loading}
className="rounded-xl border border-neutral-200 bg-white px-4 py-2.5 text-sm font-medium text-neutral-600 hover:bg-neutral-50 disabled:opacity-50"
>
Keluar
</button>
</div>
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { userRepo } from "@/server/repositories/user.repo";
import { AcceptTermsForm } from "./form";
export default async function AcceptTermsPage() {
const session = await getServerSession(authOptions);
if (!session?.user) redirect("/login?callbackUrl=/accept-terms");
// Source of truth = DB (token bisa stale).
const user = await userRepo.findById(session.user.id);
if (user?.acceptedTermsAndPrivacy) redirect("/");
return (
<div className="mx-auto max-w-xl px-4 py-10 sm:py-16">
<h1 className="text-2xl font-bold text-neutral-900 sm:text-3xl">
Satu langkah lagi
</h1>
<p className="mt-2 text-sm text-neutral-600">
Sebelum melanjutkan, mohon baca dan setujui Syarat &amp; Ketentuan dan
Kebijakan Privasi SeTrip.
</p>
<AcceptTermsForm />
</div>
);
}
+18 -1
View File
@@ -3,6 +3,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { organizerRepo } from "@/server/repositories/organizer.repo";
import { organizerService } from "@/server/services/organizer.service";
import { ReviewCard } from "@/features/organizer/components/review-card";
type Tab = "PENDING" | "APPROVED" | "REJECTED";
@@ -28,7 +29,23 @@ export default async function AdminVerificationsPage({ searchParams }: PageProps
const tab: Tab =
params.tab === "APPROVED" || params.tab === "REJECTED" ? params.tab : "PENDING";
const items = await organizerRepo.listByStatus(tab);
const rows = await organizerRepo.listByStatus(tab);
const items = rows.map((v) => ({
id: v.id,
fullName: v.fullName,
nik: organizerService.decryptNik(v.nikEncrypted),
birthDate: v.birthDate,
address: v.address,
bankName: v.bankName,
bankAccountNumber: v.bankAccountNumber,
bankAccountName: v.bankAccountName,
status: v.status,
rejectionReason: v.rejectionReason,
reviewedAt: v.reviewedAt,
createdAt: v.createdAt,
user: v.user,
reviewedBy: v.reviewedBy,
}));
const tabs: { key: Tab; label: string }[] = [
{ key: "PENDING", label: "Pending" },
+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}"`,
},
});
}
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import {
ALLOWED_KYC_MIME,
MAX_KYC_FILE_BYTES,
isKycKind,
saveEncrypted,
} from "@/lib/secure-storage";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let form: FormData;
try {
form = await req.formData();
} catch {
return NextResponse.json({ error: "Body bukan multipart/form-data" }, { status: 400 });
}
const kind = String(form.get("kind") ?? "");
const file = form.get("file");
if (!isKycKind(kind)) {
return NextResponse.json({ error: "kind harus 'ktp' atau 'selfie'" }, { status: 400 });
}
if (!(file instanceof File)) {
return NextResponse.json({ error: "File wajib diisi" }, { status: 400 });
}
if (!ALLOWED_KYC_MIME.has(file.type)) {
return NextResponse.json(
{ error: "Hanya menerima JPG, PNG, atau WebP" },
{ status: 415 },
);
}
if (file.size > MAX_KYC_FILE_BYTES) {
return NextResponse.json({ error: "File maksimal 5MB" }, { status: 413 });
}
const buf = Buffer.from(await file.arrayBuffer());
const meta = await saveEncrypted(kind, buf, file.type);
return NextResponse.json({
key: meta.key,
mime: meta.mime,
size: meta.size,
});
}
+6
View File
@@ -22,6 +22,12 @@ export * from './enums';
*
*/
export type User = Prisma.UserModel
/**
* Model Account
* Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
* Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
*/
export type Account = Prisma.AccountModel
/**
* Model OrganizerVerification
*
+6
View File
@@ -46,6 +46,12 @@ export { Prisma }
*
*/
export type User = Prisma.UserModel
/**
* Model Account
* Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
* Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
*/
export type Account = Prisma.AccountModel
/**
* Model OrganizerVerification
*
+54
View File
@@ -148,6 +148,33 @@ export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type EnumVerificationStatusFilter<$PrismaModel = never> = {
equals?: $Enums.VerificationStatus | Prisma.EnumVerificationStatusFieldRefInput<$PrismaModel>
in?: $Enums.VerificationStatus[] | Prisma.ListEnumVerificationStatusFieldRefInput<$PrismaModel>
@@ -373,6 +400,33 @@ export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type NestedEnumVerificationStatusFilter<$PrismaModel = never> = {
equals?: $Enums.VerificationStatus | Prisma.EnumVerificationStatusFieldRefInput<$PrismaModel>
in?: $Enums.VerificationStatus[] | Prisma.ListEnumVerificationStatusFieldRefInput<$PrismaModel>
File diff suppressed because one or more lines are too long
+113 -18
View File
@@ -385,6 +385,7 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
export const ModelName = {
User: 'User',
Account: 'Account',
OrganizerVerification: 'OrganizerVerification',
Trip: 'Trip',
TripReview: 'TripReview',
@@ -405,7 +406,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant"
modelProps: "user" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -483,6 +484,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
Account: {
payload: Prisma.$AccountPayload<ExtArgs>
fields: Prisma.AccountFieldRefs
operations: {
findUnique: {
args: Prisma.AccountFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload> | null
}
findUniqueOrThrow: {
args: Prisma.AccountFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
findFirst: {
args: Prisma.AccountFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload> | null
}
findFirstOrThrow: {
args: Prisma.AccountFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
findMany: {
args: Prisma.AccountFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
create: {
args: Prisma.AccountCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
createMany: {
args: Prisma.AccountCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.AccountCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
delete: {
args: Prisma.AccountDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
update: {
args: Prisma.AccountUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
deleteMany: {
args: Prisma.AccountDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.AccountUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.AccountUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>[]
}
upsert: {
args: Prisma.AccountUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$AccountPayload>
}
aggregate: {
args: Prisma.AccountAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateAccount>
}
groupBy: {
args: Prisma.AccountGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AccountGroupByOutputType>[]
}
count: {
args: Prisma.AccountCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AccountCountAggregateOutputType> | number
}
}
}
OrganizerVerification: {
payload: Prisma.$OrganizerVerificationPayload<ExtArgs>
fields: Prisma.OrganizerVerificationFieldRefs
@@ -907,15 +982,34 @@ export const UserScalarFieldEnum = {
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const AccountScalarFieldEnum = {
id: 'id',
userId: 'userId',
type: 'type',
provider: 'provider',
providerAccountId: 'providerAccountId',
refresh_token: 'refresh_token',
access_token: 'access_token',
expires_at: 'expires_at',
token_type: 'token_type',
scope: 'scope',
id_token: 'id_token',
session_state: 'session_state'
} as const
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
export const OrganizerVerificationScalarFieldEnum = {
id: 'id',
userId: 'userId',
fullName: 'fullName',
nik: 'nik',
nikEncrypted: 'nikEncrypted',
nikHash: 'nikHash',
birthDate: 'birthDate',
address: 'address',
ktpImageUrl: 'ktpImageUrl',
selfieUrl: 'selfieUrl',
ktpImageKey: 'ktpImageKey',
selfieKey: 'selfieKey',
bankName: 'bankName',
bankAccountNumber: 'bankAccountNumber',
bankAccountName: 'bankAccountName',
@@ -1056,20 +1150,6 @@ export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaM
/**
* Reference to a field of type 'VerificationStatus'
*/
export type EnumVerificationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'VerificationStatus'>
/**
* Reference to a field of type 'VerificationStatus[]'
*/
export type ListEnumVerificationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'VerificationStatus[]'>
/**
* Reference to a field of type 'Int'
*/
@@ -1084,6 +1164,20 @@ export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel,
/**
* Reference to a field of type 'VerificationStatus'
*/
export type EnumVerificationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'VerificationStatus'>
/**
* Reference to a field of type 'VerificationStatus[]'
*/
export type ListEnumVerificationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'VerificationStatus[]'>
/**
* Reference to a field of type 'TripStatus'
*/
@@ -1221,6 +1315,7 @@ export type PrismaClientOptions = ({
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
account?: Prisma.AccountOmit
organizerVerification?: Prisma.OrganizerVerificationOmit
trip?: Prisma.TripOmit
tripReview?: Prisma.TripReviewOmit
@@ -52,6 +52,7 @@ export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Account: 'Account',
OrganizerVerification: 'OrganizerVerification',
Trip: 'Trip',
TripReview: 'TripReview',
@@ -90,15 +91,34 @@ export const UserScalarFieldEnum = {
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const AccountScalarFieldEnum = {
id: 'id',
userId: 'userId',
type: 'type',
provider: 'provider',
providerAccountId: 'providerAccountId',
refresh_token: 'refresh_token',
access_token: 'access_token',
expires_at: 'expires_at',
token_type: 'token_type',
scope: 'scope',
id_token: 'id_token',
session_state: 'session_state'
} as const
export type AccountScalarFieldEnum = (typeof AccountScalarFieldEnum)[keyof typeof AccountScalarFieldEnum]
export const OrganizerVerificationScalarFieldEnum = {
id: 'id',
userId: 'userId',
fullName: 'fullName',
nik: 'nik',
nikEncrypted: 'nikEncrypted',
nikHash: 'nikHash',
birthDate: 'birthDate',
address: 'address',
ktpImageUrl: 'ktpImageUrl',
selfieUrl: 'selfieUrl',
ktpImageKey: 'ktpImageKey',
selfieKey: 'selfieKey',
bankName: 'bankName',
bankAccountNumber: 'bankAccountNumber',
bankAccountName: 'bankAccountName',
+1
View File
@@ -9,6 +9,7 @@
* 🟢 You can import this file directly.
*/
export type * from './models/User'
export type * from './models/Account'
export type * from './models/OrganizerVerification'
export type * from './models/Trip'
export type * from './models/TripReview'
File diff suppressed because it is too large Load Diff
@@ -28,11 +28,12 @@ export type OrganizerVerificationMinAggregateOutputType = {
id: string | null
userId: string | null
fullName: string | null
nik: string | null
nikEncrypted: string | null
nikHash: string | null
birthDate: Date | null
address: string | null
ktpImageUrl: string | null
selfieUrl: string | null
ktpImageKey: string | null
selfieKey: string | null
bankName: string | null
bankAccountNumber: string | null
bankAccountName: string | null
@@ -49,11 +50,12 @@ export type OrganizerVerificationMaxAggregateOutputType = {
id: string | null
userId: string | null
fullName: string | null
nik: string | null
nikEncrypted: string | null
nikHash: string | null
birthDate: Date | null
address: string | null
ktpImageUrl: string | null
selfieUrl: string | null
ktpImageKey: string | null
selfieKey: string | null
bankName: string | null
bankAccountNumber: string | null
bankAccountName: string | null
@@ -70,11 +72,12 @@ export type OrganizerVerificationCountAggregateOutputType = {
id: number
userId: number
fullName: number
nik: number
nikEncrypted: number
nikHash: number
birthDate: number
address: number
ktpImageUrl: number
selfieUrl: number
ktpImageKey: number
selfieKey: number
bankName: number
bankAccountNumber: number
bankAccountName: number
@@ -93,11 +96,12 @@ export type OrganizerVerificationMinAggregateInputType = {
id?: true
userId?: true
fullName?: true
nik?: true
nikEncrypted?: true
nikHash?: true
birthDate?: true
address?: true
ktpImageUrl?: true
selfieUrl?: true
ktpImageKey?: true
selfieKey?: true
bankName?: true
bankAccountNumber?: true
bankAccountName?: true
@@ -114,11 +118,12 @@ export type OrganizerVerificationMaxAggregateInputType = {
id?: true
userId?: true
fullName?: true
nik?: true
nikEncrypted?: true
nikHash?: true
birthDate?: true
address?: true
ktpImageUrl?: true
selfieUrl?: true
ktpImageKey?: true
selfieKey?: true
bankName?: true
bankAccountNumber?: true
bankAccountName?: true
@@ -135,11 +140,12 @@ export type OrganizerVerificationCountAggregateInputType = {
id?: true
userId?: true
fullName?: true
nik?: true
nikEncrypted?: true
nikHash?: true
birthDate?: true
address?: true
ktpImageUrl?: true
selfieUrl?: true
ktpImageKey?: true
selfieKey?: true
bankName?: true
bankAccountNumber?: true
bankAccountName?: true
@@ -229,11 +235,12 @@ export type OrganizerVerificationGroupByOutputType = {
id: string
userId: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -271,11 +278,12 @@ export type OrganizerVerificationWhereInput = {
id?: Prisma.StringFilter<"OrganizerVerification"> | string
userId?: Prisma.StringFilter<"OrganizerVerification"> | string
fullName?: Prisma.StringFilter<"OrganizerVerification"> | string
nik?: Prisma.StringFilter<"OrganizerVerification"> | string
nikEncrypted?: Prisma.StringFilter<"OrganizerVerification"> | string
nikHash?: Prisma.StringFilter<"OrganizerVerification"> | string
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -294,11 +302,12 @@ export type OrganizerVerificationOrderByWithRelationInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
fullName?: Prisma.SortOrder
nik?: Prisma.SortOrder
nikEncrypted?: Prisma.SortOrder
nikHash?: Prisma.SortOrder
birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder
ktpImageUrl?: Prisma.SortOrder
selfieUrl?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder
@@ -316,15 +325,16 @@ export type OrganizerVerificationOrderByWithRelationInput = {
export type OrganizerVerificationWhereUniqueInput = Prisma.AtLeast<{
id?: string
userId?: string
nik?: string
nikHash?: string
AND?: Prisma.OrganizerVerificationWhereInput | Prisma.OrganizerVerificationWhereInput[]
OR?: Prisma.OrganizerVerificationWhereInput[]
NOT?: Prisma.OrganizerVerificationWhereInput | Prisma.OrganizerVerificationWhereInput[]
fullName?: Prisma.StringFilter<"OrganizerVerification"> | string
nikEncrypted?: Prisma.StringFilter<"OrganizerVerification"> | string
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -337,17 +347,18 @@ export type OrganizerVerificationWhereUniqueInput = Prisma.AtLeast<{
updatedAt?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
reviewedBy?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id" | "userId" | "nik">
}, "id" | "userId" | "nikHash">
export type OrganizerVerificationOrderByWithAggregationInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
fullName?: Prisma.SortOrder
nik?: Prisma.SortOrder
nikEncrypted?: Prisma.SortOrder
nikHash?: Prisma.SortOrder
birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder
ktpImageUrl?: Prisma.SortOrder
selfieUrl?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder
@@ -370,11 +381,12 @@ export type OrganizerVerificationScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
userId?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
fullName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
nik?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
nikEncrypted?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
nikHash?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
birthDate?: Prisma.DateTimeWithAggregatesFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
ktpImageUrl?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
selfieUrl?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringWithAggregatesFilter<"OrganizerVerification"> | string
@@ -390,11 +402,12 @@ export type OrganizerVerificationScalarWhereWithAggregatesInput = {
export type OrganizerVerificationCreateInput = {
id?: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -412,11 +425,12 @@ export type OrganizerVerificationUncheckedCreateInput = {
id?: string
userId: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -432,11 +446,12 @@ export type OrganizerVerificationUncheckedCreateInput = {
export type OrganizerVerificationUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -454,11 +469,12 @@ export type OrganizerVerificationUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -475,11 +491,12 @@ export type OrganizerVerificationCreateManyInput = {
id?: string
userId: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -495,11 +512,12 @@ export type OrganizerVerificationCreateManyInput = {
export type OrganizerVerificationUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -515,11 +533,12 @@ export type OrganizerVerificationUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -551,11 +570,12 @@ export type OrganizerVerificationCountOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
fullName?: Prisma.SortOrder
nik?: Prisma.SortOrder
nikEncrypted?: Prisma.SortOrder
nikHash?: Prisma.SortOrder
birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder
ktpImageUrl?: Prisma.SortOrder
selfieUrl?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder
@@ -572,11 +592,12 @@ export type OrganizerVerificationMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
fullName?: Prisma.SortOrder
nik?: Prisma.SortOrder
nikEncrypted?: Prisma.SortOrder
nikHash?: Prisma.SortOrder
birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder
ktpImageUrl?: Prisma.SortOrder
selfieUrl?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder
@@ -593,11 +614,12 @@ export type OrganizerVerificationMinOrderByAggregateInput = {
id?: Prisma.SortOrder
userId?: Prisma.SortOrder
fullName?: Prisma.SortOrder
nik?: Prisma.SortOrder
nikEncrypted?: Prisma.SortOrder
nikHash?: Prisma.SortOrder
birthDate?: Prisma.SortOrder
address?: Prisma.SortOrder
ktpImageUrl?: Prisma.SortOrder
selfieUrl?: Prisma.SortOrder
ktpImageKey?: Prisma.SortOrder
selfieKey?: Prisma.SortOrder
bankName?: Prisma.SortOrder
bankAccountNumber?: Prisma.SortOrder
bankAccountName?: Prisma.SortOrder
@@ -691,11 +713,12 @@ export type EnumVerificationStatusFieldUpdateOperationsInput = {
export type OrganizerVerificationCreateWithoutUserInput = {
id?: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -711,11 +734,12 @@ export type OrganizerVerificationCreateWithoutUserInput = {
export type OrganizerVerificationUncheckedCreateWithoutUserInput = {
id?: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -736,11 +760,12 @@ export type OrganizerVerificationCreateOrConnectWithoutUserInput = {
export type OrganizerVerificationCreateWithoutReviewedByInput = {
id?: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -757,11 +782,12 @@ export type OrganizerVerificationUncheckedCreateWithoutReviewedByInput = {
id?: string
userId: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -797,11 +823,12 @@ export type OrganizerVerificationUpdateToOneWithWhereWithoutUserInput = {
export type OrganizerVerificationUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -817,11 +844,12 @@ export type OrganizerVerificationUpdateWithoutUserInput = {
export type OrganizerVerificationUncheckedUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -857,11 +885,12 @@ export type OrganizerVerificationScalarWhereInput = {
id?: Prisma.StringFilter<"OrganizerVerification"> | string
userId?: Prisma.StringFilter<"OrganizerVerification"> | string
fullName?: Prisma.StringFilter<"OrganizerVerification"> | string
nik?: Prisma.StringFilter<"OrganizerVerification"> | string
nikEncrypted?: Prisma.StringFilter<"OrganizerVerification"> | string
nikHash?: Prisma.StringFilter<"OrganizerVerification"> | string
birthDate?: Prisma.DateTimeFilter<"OrganizerVerification"> | Date | string
address?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieUrl?: Prisma.StringFilter<"OrganizerVerification"> | string
ktpImageKey?: Prisma.StringFilter<"OrganizerVerification"> | string
selfieKey?: Prisma.StringFilter<"OrganizerVerification"> | string
bankName?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountNumber?: Prisma.StringFilter<"OrganizerVerification"> | string
bankAccountName?: Prisma.StringFilter<"OrganizerVerification"> | string
@@ -878,11 +907,12 @@ export type OrganizerVerificationCreateManyReviewedByInput = {
id?: string
userId: string
fullName: string
nik: string
nikEncrypted: string
nikHash: string
birthDate: Date | string
address: string
ktpImageUrl: string
selfieUrl: string
ktpImageKey: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -897,11 +927,12 @@ export type OrganizerVerificationCreateManyReviewedByInput = {
export type OrganizerVerificationUpdateWithoutReviewedByInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -918,11 +949,12 @@ export type OrganizerVerificationUncheckedUpdateWithoutReviewedByInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -938,11 +970,12 @@ export type OrganizerVerificationUncheckedUpdateManyWithoutReviewedByInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
userId?: Prisma.StringFieldUpdateOperationsInput | string
fullName?: Prisma.StringFieldUpdateOperationsInput | string
nik?: Prisma.StringFieldUpdateOperationsInput | string
nikEncrypted?: Prisma.StringFieldUpdateOperationsInput | string
nikHash?: Prisma.StringFieldUpdateOperationsInput | string
birthDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
address?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageUrl?: Prisma.StringFieldUpdateOperationsInput | string
selfieUrl?: Prisma.StringFieldUpdateOperationsInput | string
ktpImageKey?: Prisma.StringFieldUpdateOperationsInput | string
selfieKey?: Prisma.StringFieldUpdateOperationsInput | string
bankName?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountNumber?: Prisma.StringFieldUpdateOperationsInput | string
bankAccountName?: Prisma.StringFieldUpdateOperationsInput | string
@@ -960,11 +993,12 @@ export type OrganizerVerificationSelect<ExtArgs extends runtime.Types.Extensions
id?: boolean
userId?: boolean
fullName?: boolean
nik?: boolean
nikEncrypted?: boolean
nikHash?: boolean
birthDate?: boolean
address?: boolean
ktpImageUrl?: boolean
selfieUrl?: boolean
ktpImageKey?: boolean
selfieKey?: boolean
bankName?: boolean
bankAccountNumber?: boolean
bankAccountName?: boolean
@@ -983,11 +1017,12 @@ export type OrganizerVerificationSelectCreateManyAndReturn<ExtArgs extends runti
id?: boolean
userId?: boolean
fullName?: boolean
nik?: boolean
nikEncrypted?: boolean
nikHash?: boolean
birthDate?: boolean
address?: boolean
ktpImageUrl?: boolean
selfieUrl?: boolean
ktpImageKey?: boolean
selfieKey?: boolean
bankName?: boolean
bankAccountNumber?: boolean
bankAccountName?: boolean
@@ -1006,11 +1041,12 @@ export type OrganizerVerificationSelectUpdateManyAndReturn<ExtArgs extends runti
id?: boolean
userId?: boolean
fullName?: boolean
nik?: boolean
nikEncrypted?: boolean
nikHash?: boolean
birthDate?: boolean
address?: boolean
ktpImageUrl?: boolean
selfieUrl?: boolean
ktpImageKey?: boolean
selfieKey?: boolean
bankName?: boolean
bankAccountNumber?: boolean
bankAccountName?: boolean
@@ -1029,11 +1065,12 @@ export type OrganizerVerificationSelectScalar = {
id?: boolean
userId?: boolean
fullName?: boolean
nik?: boolean
nikEncrypted?: boolean
nikHash?: boolean
birthDate?: boolean
address?: boolean
ktpImageUrl?: boolean
selfieUrl?: boolean
ktpImageKey?: boolean
selfieKey?: boolean
bankName?: boolean
bankAccountNumber?: boolean
bankAccountName?: boolean
@@ -1046,7 +1083,7 @@ export type OrganizerVerificationSelectScalar = {
updatedAt?: boolean
}
export type OrganizerVerificationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "fullName" | "nik" | "birthDate" | "address" | "ktpImageUrl" | "selfieUrl" | "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" | "selfieKey" | "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> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
reviewedBy?: boolean | Prisma.OrganizerVerification$reviewedByArgs<ExtArgs>
@@ -1074,19 +1111,23 @@ export type $OrganizerVerificationPayload<ExtArgs extends runtime.Types.Extensio
*/
fullName: string
/**
* Nomor Induk Kependudukan (PII — perlakukan sensitif)
* NIK terenkripsi (AES-256-GCM, base64). Plaintext tidak disimpan.
*/
nik: string
nikEncrypted: string
/**
* HMAC-SHA256(NIK + pepper) untuk uniqueness lookup tanpa membuka plaintext.
*/
nikHash: string
birthDate: Date
address: string
/**
* URL foto KTP (untuk MVP pakai hosting; pindah ke storage privat untuk produksi)
* Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public.
*/
ktpImageUrl: string
ktpImageKey: string
/**
* URL selfie memegang KTP
* Storage key selfie memegang KTP.
*/
selfieUrl: string
selfieKey: string
bankName: string
bankAccountNumber: string
bankAccountName: string
@@ -1525,11 +1566,12 @@ export interface OrganizerVerificationFieldRefs {
readonly id: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly userId: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly fullName: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly nik: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly nikEncrypted: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly nikHash: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly birthDate: Prisma.FieldRef<"OrganizerVerification", 'DateTime'>
readonly address: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly ktpImageUrl: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly selfieUrl: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly ktpImageKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly selfieKey: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankName: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankAccountNumber: Prisma.FieldRef<"OrganizerVerification", 'String'>
readonly bankAccountName: Prisma.FieldRef<"OrganizerVerification", 'String'>
+200 -35
View File
@@ -175,7 +175,7 @@ export type UserGroupByOutputType = {
id: string
name: string
email: string
password: string
password: string | null
image: string | null
acceptedTermsAndPrivacy: boolean
acceptedAt: Date | null
@@ -208,12 +208,13 @@ export type UserWhereInput = {
id?: Prisma.StringFilter<"User"> | string
name?: Prisma.StringFilter<"User"> | string
email?: Prisma.StringFilter<"User"> | string
password?: Prisma.StringFilter<"User"> | string
password?: Prisma.StringNullableFilter<"User"> | string | null
image?: Prisma.StringNullableFilter<"User"> | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFilter<"User"> | boolean
acceptedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
accounts?: Prisma.AccountListRelationFilter
trips?: Prisma.TripListRelationFilter
participations?: Prisma.TripParticipantListRelationFilter
tripReviews?: Prisma.TripReviewListRelationFilter
@@ -225,12 +226,13 @@ export type UserOrderByWithRelationInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
password?: Prisma.SortOrder
password?: Prisma.SortOrderInput | Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
acceptedTermsAndPrivacy?: Prisma.SortOrder
acceptedAt?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
accounts?: Prisma.AccountOrderByRelationAggregateInput
trips?: Prisma.TripOrderByRelationAggregateInput
participations?: Prisma.TripParticipantOrderByRelationAggregateInput
tripReviews?: Prisma.TripReviewOrderByRelationAggregateInput
@@ -245,12 +247,13 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
OR?: Prisma.UserWhereInput[]
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
name?: Prisma.StringFilter<"User"> | string
password?: Prisma.StringFilter<"User"> | string
password?: Prisma.StringNullableFilter<"User"> | string | null
image?: Prisma.StringNullableFilter<"User"> | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFilter<"User"> | boolean
acceptedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
accounts?: Prisma.AccountListRelationFilter
trips?: Prisma.TripListRelationFilter
participations?: Prisma.TripParticipantListRelationFilter
tripReviews?: Prisma.TripReviewListRelationFilter
@@ -262,7 +265,7 @@ export type UserOrderByWithAggregationInput = {
id?: Prisma.SortOrder
name?: Prisma.SortOrder
email?: Prisma.SortOrder
password?: Prisma.SortOrder
password?: Prisma.SortOrderInput | Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
acceptedTermsAndPrivacy?: Prisma.SortOrder
acceptedAt?: Prisma.SortOrderInput | Prisma.SortOrder
@@ -280,7 +283,7 @@ export type UserScalarWhereWithAggregatesInput = {
id?: Prisma.StringWithAggregatesFilter<"User"> | string
name?: Prisma.StringWithAggregatesFilter<"User"> | string
email?: Prisma.StringWithAggregatesFilter<"User"> | string
password?: Prisma.StringWithAggregatesFilter<"User"> | string
password?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
acceptedTermsAndPrivacy?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
acceptedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
@@ -292,12 +295,13 @@ export type UserCreateInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
@@ -309,12 +313,13 @@ export type UserUncheckedCreateInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
@@ -326,12 +331,13 @@ export type UserUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
@@ -343,12 +349,13 @@ export type UserUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
@@ -360,7 +367,7 @@ export type UserCreateManyInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
@@ -372,7 +379,7 @@ export type UserUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -384,7 +391,7 @@ export type UserUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
@@ -458,6 +465,20 @@ export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
export type UserCreateNestedOneWithoutAccountsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutAccountsInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutAccountsNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutAccountsInput
upsert?: Prisma.UserUpsertWithoutAccountsInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutAccountsInput, Prisma.UserUpdateWithoutAccountsInput>, Prisma.UserUncheckedUpdateWithoutAccountsInput>
}
export type UserCreateNestedOneWithoutOrganizerVerificationInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutOrganizerVerificationInput, Prisma.UserUncheckedCreateWithoutOrganizerVerificationInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutOrganizerVerificationInput
@@ -530,11 +551,11 @@ export type UserUpdateOneRequiredWithoutParticipationsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutParticipationsInput, Prisma.UserUpdateWithoutParticipationsInput>, Prisma.UserUncheckedUpdateWithoutParticipationsInput>
}
export type UserCreateWithoutOrganizerVerificationInput = {
export type UserCreateWithoutAccountsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
@@ -543,6 +564,91 @@ export type UserCreateWithoutOrganizerVerificationInput = {
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
}
export type UserUncheckedCreateWithoutAccountsInput = {
id?: string
name: string
email: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
}
export type UserCreateOrConnectWithoutAccountsInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
}
export type UserUpsertWithoutAccountsInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutAccountsInput, Prisma.UserUncheckedUpdateWithoutAccountsInput>
create: Prisma.XOR<Prisma.UserCreateWithoutAccountsInput, Prisma.UserUncheckedCreateWithoutAccountsInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutAccountsInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutAccountsInput, Prisma.UserUncheckedUpdateWithoutAccountsInput>
}
export type UserUpdateWithoutAccountsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
}
export type UserUncheckedUpdateWithoutAccountsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
}
export type UserCreateWithoutOrganizerVerificationInput = {
id?: string
name: string
email: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
}
@@ -550,12 +656,13 @@ export type UserUncheckedCreateWithoutOrganizerVerificationInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
@@ -571,12 +678,13 @@ export type UserCreateWithoutReviewedVerificationsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
@@ -587,12 +695,13 @@ export type UserUncheckedCreateWithoutReviewedVerificationsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
@@ -619,12 +728,13 @@ export type UserUpdateWithoutOrganizerVerificationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
@@ -635,12 +745,13 @@ export type UserUncheckedUpdateWithoutOrganizerVerificationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
@@ -662,12 +773,13 @@ export type UserUpdateWithoutReviewedVerificationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
@@ -678,12 +790,13 @@ export type UserUncheckedUpdateWithoutReviewedVerificationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
@@ -694,12 +807,13 @@ export type UserCreateWithoutTripsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
@@ -710,12 +824,13 @@ export type UserUncheckedCreateWithoutTripsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
@@ -742,12 +857,13 @@ export type UserUpdateWithoutTripsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
@@ -758,12 +874,13 @@ export type UserUncheckedUpdateWithoutTripsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
@@ -774,12 +891,13 @@ export type UserCreateWithoutTripReviewsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
@@ -790,12 +908,13 @@ export type UserUncheckedCreateWithoutTripReviewsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
@@ -822,12 +941,13 @@ export type UserUpdateWithoutTripReviewsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
@@ -838,12 +958,13 @@ export type UserUncheckedUpdateWithoutTripReviewsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
@@ -854,12 +975,13 @@ export type UserCreateWithoutParticipationsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
@@ -870,12 +992,13 @@ export type UserUncheckedCreateWithoutParticipationsInput = {
id?: string
name: string
email: string
password: string
password?: string | null
image?: string | null
acceptedTermsAndPrivacy?: boolean
acceptedAt?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
@@ -902,12 +1025,13 @@ export type UserUpdateWithoutParticipationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
@@ -918,12 +1042,13 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
@@ -936,6 +1061,7 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
*/
export type UserCountOutputType = {
accounts: number
trips: number
participations: number
tripReviews: number
@@ -943,6 +1069,7 @@ export type UserCountOutputType = {
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
accounts?: boolean | UserCountOutputTypeCountAccountsArgs
trips?: boolean | UserCountOutputTypeCountTripsArgs
participations?: boolean | UserCountOutputTypeCountParticipationsArgs
tripReviews?: boolean | UserCountOutputTypeCountTripReviewsArgs
@@ -959,6 +1086,13 @@ export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensi
select?: Prisma.UserCountOutputTypeSelect<ExtArgs> | null
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountAccountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.AccountWhereInput
}
/**
* UserCountOutputType without action
*/
@@ -998,6 +1132,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
acceptedAt?: boolean
createdAt?: boolean
updatedAt?: boolean
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
@@ -1044,6 +1179,7 @@ export type UserSelectScalar = {
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "image" | "acceptedTermsAndPrivacy" | "acceptedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
trips?: boolean | Prisma.User$tripsArgs<ExtArgs>
participations?: boolean | Prisma.User$participationsArgs<ExtArgs>
tripReviews?: boolean | Prisma.User$tripReviewsArgs<ExtArgs>
@@ -1057,6 +1193,7 @@ export type UserIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "User"
objects: {
accounts: Prisma.$AccountPayload<ExtArgs>[]
trips: Prisma.$TripPayload<ExtArgs>[]
participations: Prisma.$TripParticipantPayload<ExtArgs>[]
tripReviews: Prisma.$TripReviewPayload<ExtArgs>[]
@@ -1067,7 +1204,10 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
id: string
name: string
email: string
password: string
/**
* Hash bcrypt. Null untuk user yang sign-in via OAuth (mis. Google).
*/
password: string | null
image: string | null
/**
* Apakah user telah menyetujui Syarat & Ketentuan dan Kebijakan Privasi
@@ -1473,6 +1613,7 @@ readonly fields: UserFieldRefs;
*/
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
accounts<T extends Prisma.User$accountsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$accountsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$AccountPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
trips<T extends Prisma.User$tripsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
participations<T extends Prisma.User$participationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$participationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
tripReviews<T extends Prisma.User$tripReviewsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$tripReviewsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$TripReviewPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
@@ -1908,6 +2049,30 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* User.accounts
*/
export type User$accountsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Account
*/
select?: Prisma.AccountSelect<ExtArgs> | null
/**
* Omit specific fields from the Account
*/
omit?: Prisma.AccountOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.AccountInclude<ExtArgs> | null
where?: Prisma.AccountWhereInput
orderBy?: Prisma.AccountOrderByWithRelationInput | Prisma.AccountOrderByWithRelationInput[]
cursor?: Prisma.AccountWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
}
/**
* User.trips
*/
+9
View File
@@ -5,6 +5,7 @@ import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { GoogleSignInButton } from "@/components/shared/google-sign-in-button";
function safeInternalPath(raw: string | null): string {
if (!raw || !raw.startsWith("/") || raw.startsWith("//")) return "/";
@@ -84,6 +85,14 @@ function LoginForm() {
</div>
)}
<GoogleSignInButton callbackUrl={safeInternalPath(searchParams.get("callbackUrl"))} />
<div className="my-4 flex items-center gap-3 text-xs text-neutral-400">
<span className="h-px flex-1 bg-neutral-200" />
<span>atau</span>
<span className="h-px flex-1 bg-neutral-200" />
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-semibold text-neutral-700">
+9
View File
@@ -6,6 +6,7 @@ import { signIn } from "next-auth/react";
import Link from "next/link";
import Image from "next/image";
import { registerAction } from "@/features/auth/actions";
import { GoogleSignInButton } from "@/components/shared/google-sign-in-button";
export default function RegisterPage() {
const router = useRouter();
@@ -83,6 +84,14 @@ export default function RegisterPage() {
</div>
)}
<GoogleSignInButton label="Daftar dengan Google" />
<div className="my-4 flex items-center gap-3 text-xs text-neutral-400">
<span className="h-px flex-1 bg-neutral-200" />
<span>atau</span>
<span className="h-px flex-1 bg-neutral-200" />
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="mb-1.5 block text-sm font-semibold text-neutral-700">
+15 -1
View File
@@ -14,6 +14,20 @@ export default async function VerifyPage() {
const verification = await organizerService.getStatusForUser(session.user.id);
const initial = verification
? {
fullName: verification.fullName,
nik: organizerService.decryptNik(verification.nikEncrypted),
birthDate: verification.birthDate,
address: verification.address,
ktpImageKey: verification.ktpImageKey,
selfieKey: verification.selfieKey,
bankName: verification.bankName,
bankAccountNumber: verification.bankAccountNumber,
bankAccountName: verification.bankAccountName,
}
: null;
return (
<div className="mx-auto max-w-2xl px-4 py-8 sm:py-12">
<div className="mb-6">
@@ -66,7 +80,7 @@ export default async function VerifyPage() {
)}
{verification?.status !== "APPROVED" && verification?.status !== "PENDING" && (
<VerifyForm initial={verification ?? null} />
<VerifyForm initial={initial} />
)}
<p className="mt-6 text-center text-sm text-neutral-500">
@@ -0,0 +1,46 @@
"use client";
import { signIn } from "next-auth/react";
import { useState } from "react";
export function GoogleSignInButton({
callbackUrl = "/",
label = "Lanjutkan dengan Google",
}: {
callbackUrl?: string;
label?: string;
}) {
const [loading, setLoading] = useState(false);
return (
<button
type="button"
disabled={loading}
onClick={() => {
setLoading(true);
signIn("google", { callbackUrl });
}}
className="flex w-full items-center justify-center gap-2 rounded-xl border border-neutral-200 bg-white py-2.5 text-sm font-semibold text-neutral-700 shadow-sm transition-colors hover:bg-neutral-50 disabled:opacity-50"
>
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden>
<path
fill="#4285F4"
d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.13 4.13 0 0 1-1.79 2.71v2.26h2.9c1.7-1.57 2.69-3.88 2.69-6.61z"
/>
<path
fill="#34A853"
d="M9 18c2.43 0 4.47-.81 5.96-2.18l-2.9-2.26c-.8.54-1.83.86-3.06.86-2.35 0-4.34-1.59-5.05-3.72H.96v2.34A9 9 0 0 0 9 18z"
/>
<path
fill="#FBBC05"
d="M3.95 10.7A5.41 5.41 0 0 1 3.66 9c0-.59.1-1.16.29-1.7V4.96H.96A9 9 0 0 0 0 9c0 1.45.35 2.82.96 4.04l2.99-2.34z"
/>
<path
fill="#EA4335"
d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A9 9 0 0 0 9 0 9 9 0 0 0 .96 4.96l2.99 2.34C4.66 5.17 6.65 3.58 9 3.58z"
/>
</svg>
{loading ? "Menghubungkan..." : label}
</button>
);
}
-2
View File
@@ -1,5 +1,3 @@
version: "3.9"
services:
postgres:
image: postgres:15
+13 -1
View File
@@ -2,4 +2,16 @@ DATABASE_URL="postgresql://setrip_user:setrip_password@localhost:5432/setrip_db"
NEXTAUTH_SECRET="3GaP/mqi1IYbafyLfyI54ouPRDE0IUK5vFqpKJQM5hg="
NEXTAUTH_URL="http://localhost:3000"
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)
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
KYC_ENCRYPTION_KEY=
# 32-byte hex secret used as HMAC pepper for NIK uniqueness lookup
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
KYC_NIK_PEPPER=
# Absolute path for private KYC uploads (default: <cwd>/uploads/private)
KYC_UPLOAD_DIR=
GOOGLE_CLIENT_ID="xxxxxxxx"
GOOGLE_CLIENT_SECRET="xxxxxxxx"
+16
View File
@@ -1,7 +1,10 @@
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { registerSchema } from "./schemas";
import { authService } from "@/server/services/auth.service";
import { userRepo } from "@/server/repositories/user.repo";
export async function registerAction(formData: FormData) {
const raw = {
@@ -29,3 +32,16 @@ export async function registerAction(formData: FormData) {
return { error: (err as Error).message };
}
}
export async function acceptTermsAction() {
const session = await getServerSession(authOptions);
if (!session?.user) {
return { error: "Kamu harus login terlebih dahulu" };
}
try {
await userRepo.markAcceptedTerms(session.user.id);
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
}
+2 -2
View File
@@ -18,8 +18,8 @@ export async function submitVerificationAction(formData: FormData) {
nik: formData.get("nik") as string,
birthDate: formData.get("birthDate") as string,
address: formData.get("address") as string,
ktpImageUrl: formData.get("ktpImageUrl") as string,
selfieUrl: formData.get("selfieUrl") as string,
ktpImageKey: formData.get("ktpImageKey") as string,
selfieKey: formData.get("selfieKey") as string,
bankName: formData.get("bankName") as string,
bankAccountNumber: formData.get("bankAccountNumber") as string,
bankAccountName: formData.get("bankAccountName") as string,
+16 -13
View File
@@ -2,17 +2,15 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { reviewVerificationAction } from "@/features/organizer/actions";
type Verification = {
id: string;
fullName: string;
/** NIK plaintext, sudah di-decrypt di server sebelum sampai ke komponen ini. */
nik: string;
birthDate: Date;
address: string;
ktpImageUrl: string;
selfieUrl: string;
bankName: string;
bankAccountNumber: string;
bankAccountName: string;
@@ -90,8 +88,14 @@ export function ReviewCard({ verification }: { verification: Verification }) {
</div>
<div className="mt-5 grid gap-4 sm:grid-cols-2">
<ImagePreview label="Foto KTP" url={verification.ktpImageUrl} />
<ImagePreview label="Selfie + KTP" url={verification.selfieUrl} />
<ImagePreview
label="Foto KTP"
src={`/api/files/kyc/${verification.id}/ktp`}
/>
<ImagePreview
label="Selfie + KTP"
src={`/api/files/kyc/${verification.id}/selfie`}
/>
</div>
{verification.status === "REJECTED" && verification.rejectionReason && (
@@ -196,26 +200,25 @@ function Field({
);
}
function ImagePreview({ label, url }: { label: string; url: string }) {
function ImagePreview({ label, src }: { label: string; src: string }) {
return (
<div>
<p className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
{label}
</p>
<a
href={url}
href={src}
target="_blank"
rel="noopener noreferrer"
className="block overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100"
>
<div className="relative aspect-[4/3] w-full">
<Image
src={url}
{/* Secure endpoint sends Cache-Control: private,no-store. Use plain <img> to skip Next/Image optimizer. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={label}
fill
unoptimized
className="object-cover"
sizes="(min-width: 640px) 50vw, 100vw"
className="h-full w-full object-cover"
/>
</div>
</a>
+119 -30
View File
@@ -9,13 +9,18 @@ type Initial = {
nik: string;
birthDate: Date;
address: string;
ktpImageUrl: string;
selfieUrl: string;
ktpImageKey: string;
selfieKey: string;
bankName: string;
bankAccountNumber: string;
bankAccountName: string;
} | null;
type UploadKind = "ktp" | "selfie";
const ACCEPT_MIME = "image/jpeg,image/png,image/webp";
const MAX_BYTES = 5 * 1024 * 1024;
function toYmd(d: Date): string {
const y = d.getUTCFullYear();
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
@@ -27,12 +32,20 @@ export function VerifyForm({ initial }: { initial: Initial }) {
const router = useRouter();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [ktpKey, setKtpKey] = useState(initial?.ktpImageKey ?? "");
const [selfieKey, setSelfieKey] = useState(initial?.selfieKey ?? "");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
if (!ktpKey || !selfieKey) {
setError("Foto KTP dan selfie wajib diunggah");
return;
}
setLoading(true);
const formData = new FormData(e.currentTarget);
formData.set("ktpImageKey", ktpKey);
formData.set("selfieKey", selfieKey);
const result = await submitVerificationAction(formData);
setLoading(false);
if (result.error) {
@@ -119,36 +132,24 @@ export function VerifyForm({ initial }: { initial: Initial }) {
<section>
<h2 className="mb-3 text-base font-bold text-neutral-900">🖼 Foto</h2>
<p className="mb-3 text-xs text-neutral-500">
Upload foto ke hosting (imgur, imgbb, dll) lalu paste URL-nya. Foto akan
dilihat tim admin saat review.
Foto disimpan terenkripsi di server SeTrip dan hanya bisa dilihat oleh
tim admin saat review. Maks 5MB, JPG/PNG/WebP.
</p>
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
URL Foto KTP
</label>
<input
name="ktpImageUrl"
type="url"
required
defaultValue={initial?.ktpImageUrl ?? ""}
className={inputCls}
placeholder="https://i.imgur.com/xxxx.jpg"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
URL Selfie dengan KTP
</label>
<input
name="selfieUrl"
type="url"
required
defaultValue={initial?.selfieUrl ?? ""}
className={inputCls}
placeholder="https://i.imgur.com/yyyy.jpg"
/>
</div>
<FileUpload
label="Foto KTP"
kind="ktp"
value={ktpKey}
onChange={setKtpKey}
onError={setError}
/>
<FileUpload
label="Selfie dengan KTP"
kind="selfie"
value={selfieKey}
onChange={setSelfieKey}
onError={setError}
/>
</div>
</section>
@@ -213,3 +214,91 @@ export function VerifyForm({ initial }: { initial: Initial }) {
</form>
);
}
function FileUpload({
label,
kind,
value,
onChange,
onError,
}: {
label: string;
kind: UploadKind;
value: string;
onChange: (key: string) => void;
onError: (msg: string) => void;
}) {
const [busy, setBusy] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string>("");
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > MAX_BYTES) {
onError(`${label} maksimal 5MB`);
e.target.value = "";
return;
}
if (!ACCEPT_MIME.split(",").includes(file.type)) {
onError(`${label} harus JPG, PNG, atau WebP`);
e.target.value = "";
return;
}
setBusy(true);
onError("");
try {
const fd = new FormData();
fd.set("kind", kind);
fd.set("file", file);
const res = await fetch("/api/upload/kyc", { method: "POST", body: fd });
const json = await res.json();
if (!res.ok) {
onError(json.error ?? `Gagal mengunggah ${label}`);
return;
}
onChange(json.key);
const obj = URL.createObjectURL(file);
setPreviewUrl((old) => {
if (old) URL.revokeObjectURL(old);
return obj;
});
} catch {
onError(`Gagal mengunggah ${label}`);
} finally {
setBusy(false);
e.target.value = "";
}
}
return (
<div>
<label className="mb-1.5 block text-sm font-semibold text-neutral-700">
{label}
</label>
<div className="flex items-center gap-3">
<label className="inline-flex cursor-pointer items-center rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">
{busy ? "Mengunggah..." : value ? "Ganti file" : "Pilih file"}
<input
type="file"
accept={ACCEPT_MIME}
onChange={onPick}
disabled={busy}
className="sr-only"
/>
</label>
{value && !busy && (
<span className="text-xs text-neutral-500"> Terunggah</span>
)}
</div>
{previewUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={previewUrl}
alt={`${label} preview`}
className="mt-2 max-h-40 rounded-lg border border-neutral-200"
/>
)}
</div>
);
}
+4 -8
View File
@@ -22,18 +22,14 @@ export const submitVerificationSchema = z.object({
.trim()
.min(5, "Alamat minimal 5 karakter")
.max(LIMITS.MAX_ADDRESS_LENGTH, `Alamat maksimal ${LIMITS.MAX_ADDRESS_LENGTH} karakter`),
ktpImageUrl: z
ktpImageKey: z
.string()
.trim()
.min(1, "Foto KTP wajib diisi")
.max(LIMITS.MAX_URL_LENGTH, "URL foto KTP terlalu panjang")
.pipe(z.url("URL foto KTP tidak valid")),
selfieUrl: z
.regex(/^ktp\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto KTP wajib diunggah"),
selfieKey: z
.string()
.trim()
.min(1, "Foto selfie dengan KTP wajib diisi")
.max(LIMITS.MAX_URL_LENGTH, "URL foto selfie terlalu panjang")
.pipe(z.url("URL foto selfie tidak valid")),
.regex(/^selfie\/[A-Za-z0-9_-]+\.(jpg|png|webp)$/, "Foto selfie wajib diunggah"),
bankName: z
.string()
.trim()
+27 -1
View File
@@ -1,10 +1,22 @@
import { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
// Adapter dipakai untuk persist User + Account saat OAuth (Google).
// Session tetap pakai JWT supaya kompatibel dengan CredentialsProvider.
export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// Auto-link kalau email Google sama dengan email user yang sudah register
// via Credentials. Aman karena Google selalu memverifikasi email pemilik akun.
allowDangerousEmailAccountLinking: true,
}),
CredentialsProvider({
name: "credentials",
credentials: {
@@ -24,6 +36,10 @@ export const authOptions: AuthOptions = {
throw new Error("Email tidak ditemukan");
}
if (!user.password) {
throw new Error("Akun ini terdaftar via Google. Silakan login dengan Google.");
}
const isPasswordValid = await bcrypt.compare(
credentials.password,
user.password
@@ -46,15 +62,25 @@ export const authOptions: AuthOptions = {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
async jwt({ token, user, trigger }) {
if (user) {
token.id = user.id;
}
// Hidrasi `acceptedTermsAndPrivacy` dari DB pada login pertama dan setiap
// kali client memanggil `useSession().update()` (setelah user accept).
if (token.id && (trigger === "update" || token.acceptedTermsAndPrivacy === undefined)) {
const dbUser = await prisma.user.findUnique({
where: { id: token.id as string },
select: { acceptedTermsAndPrivacy: true },
});
token.acceptedTermsAndPrivacy = dbUser?.acceptedTermsAndPrivacy ?? false;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
session.user.acceptedTermsAndPrivacy = token.acceptedTermsAndPrivacy ?? false;
}
return session;
},
+55
View File
@@ -0,0 +1,55 @@
import crypto from "node:crypto";
const ALGO = "aes-256-gcm";
const IV_LEN = 12;
const TAG_LEN = 16;
function readKey(envName: string): Buffer {
const hex = process.env[envName];
if (!hex) throw new Error(`Missing env ${envName}`);
if (hex.length !== 64) {
throw new Error(`${envName} must be 64 hex chars (32 bytes)`);
}
return Buffer.from(hex, "hex");
}
function getEncKey(): Buffer {
return readKey("KYC_ENCRYPTION_KEY");
}
function getNikPepper(): Buffer {
return readKey("KYC_NIK_PEPPER");
}
/** Encrypt a Buffer with AES-256-GCM. Output layout: [iv(12) | tag(16) | ciphertext]. */
export function encryptBuffer(plain: Buffer): Buffer {
const iv = crypto.randomBytes(IV_LEN);
const cipher = crypto.createCipheriv(ALGO, getEncKey(), iv);
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, ct]);
}
export function decryptBuffer(blob: Buffer): Buffer {
if (blob.length < IV_LEN + TAG_LEN) throw new Error("Ciphertext too short");
const iv = blob.subarray(0, IV_LEN);
const tag = blob.subarray(IV_LEN, IV_LEN + TAG_LEN);
const ct = blob.subarray(IV_LEN + TAG_LEN);
const decipher = crypto.createDecipheriv(ALGO, getEncKey(), iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
/** Encrypt UTF-8 string -> base64 string. Used for short PII like NIK. */
export function encryptString(plain: string): string {
return encryptBuffer(Buffer.from(plain, "utf8")).toString("base64");
}
export function decryptString(b64: string): string {
return decryptBuffer(Buffer.from(b64, "base64")).toString("utf8");
}
/** Deterministic HMAC-SHA256 of a normalized value, hex-encoded. Used for unique-lookup of NIK without storing plaintext. */
export function hmacHex(value: string): string {
return crypto.createHmac("sha256", getNikPepper()).update(value).digest("hex");
}
+102
View File
@@ -0,0 +1,102 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { encryptBuffer, decryptBuffer } from "@/lib/crypto";
export type KycKind = "ktp" | "selfie";
const KIND_DIRS: Record<KycKind, string> = {
ktp: "ktp",
selfie: "selfie",
};
/** Bytes. ~5MB matches the form limit; raise here if you change the upload route. */
export const MAX_KYC_FILE_BYTES = 5 * 1024 * 1024;
export const ALLOWED_KYC_MIME = new Set([
"image/jpeg",
"image/png",
"image/webp",
]);
const EXT_BY_MIME: Record<string, string> = {
"image/jpeg": "jpg",
"image/png": "png",
"image/webp": "webp",
};
function rootDir(): string {
const fromEnv = process.env.KYC_UPLOAD_DIR;
if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
return path.join(process.cwd(), "uploads", "private");
}
function dirFor(kind: KycKind): string {
return path.join(rootDir(), KIND_DIRS[kind]);
}
/** Storage key written into DB: `<kind>/<id>.<ext>`. The kind segment is enforced to match the route. */
export type StoredFileMeta = {
key: string;
mime: string;
size: number;
};
export function isKycKind(value: string): value is KycKind {
return value === "ktp" || value === "selfie";
}
/** Resolve a storage key (`ktp/abc.jpg`) to an absolute path inside the upload dir. Throws on traversal. */
function resolveKey(kind: KycKind, key: string): string {
const expectedPrefix = `${KIND_DIRS[kind]}/`;
if (!key.startsWith(expectedPrefix)) {
throw new Error("Storage key does not match kind");
}
const relative = key.slice(expectedPrefix.length);
if (!/^[A-Za-z0-9_-]+\.(jpg|png|webp)$/.test(relative)) {
throw new Error("Storage key has invalid characters");
}
const abs = path.join(dirFor(kind), relative);
const dir = dirFor(kind);
if (!abs.startsWith(dir + path.sep) && abs !== dir) {
throw new Error("Storage key escapes upload directory");
}
return abs;
}
export async function saveEncrypted(
kind: KycKind,
data: Buffer,
mime: string,
): Promise<StoredFileMeta> {
if (!ALLOWED_KYC_MIME.has(mime)) throw new Error("Tipe file tidak didukung");
if (data.length === 0) throw new Error("File kosong");
if (data.length > MAX_KYC_FILE_BYTES) throw new Error("File terlalu besar");
const ext = EXT_BY_MIME[mime];
const id = crypto.randomBytes(16).toString("hex");
const key = `${KIND_DIRS[kind]}/${id}.${ext}`;
const abs = resolveKey(kind, key);
await fs.mkdir(path.dirname(abs), { recursive: true });
const blob = encryptBuffer(data);
await fs.writeFile(abs, blob, { mode: 0o600 });
return { key, mime, size: data.length };
}
export async function readDecrypted(kind: KycKind, key: string): Promise<Buffer> {
const abs = resolveKey(kind, key);
const blob = await fs.readFile(abs);
return decryptBuffer(blob);
}
export async function deleteFile(kind: KycKind, key: string): Promise<void> {
const abs = resolveKey(kind, key);
await fs.rm(abs, { force: true });
}
export function mimeFromKey(key: string): string {
if (key.endsWith(".jpg")) return "image/jpeg";
if (key.endsWith(".png")) return "image/png";
if (key.endsWith(".webp")) return "image/webp";
return "application/octet-stream";
}
+11
View File
@@ -8,6 +8,7 @@
"name": "setrip",
"version": "0.2.0",
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"axios": "^1.15.0",
@@ -1645,6 +1646,16 @@
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@next-auth/prisma-adapter": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz",
"integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==",
"license": "ISC",
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
}
},
"node_modules/@next/env": {
"version": "16.2.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz",
+1
View File
@@ -13,6 +13,7 @@
"seed": "npx tsx prisma/seed.ts"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"axios": "^1.15.0",
@@ -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",
+37
View File
@@ -0,0 +1,37 @@
import { NextResponse, type NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
// Path yang boleh diakses oleh user yang login tapi belum accept Terms & Privacy.
const ALLOWED_WHEN_NOT_ACCEPTED = [
"/accept-terms",
"/terms",
"/privacy",
];
export async function proxy(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
if (!token) return NextResponse.next();
if (token.acceptedTermsAndPrivacy) return NextResponse.next();
const { pathname } = req.nextUrl;
if (pathname.startsWith("/api/auth")) return NextResponse.next();
if (
ALLOWED_WHEN_NOT_ACCEPTED.some(
(p) => pathname === p || pathname.startsWith(`${p}/`),
)
) {
return NextResponse.next();
}
const url = req.nextUrl.clone();
url.pathname = "/accept-terms";
url.search = "";
return NextResponse.redirect(url);
}
export const config = {
matcher: [
// Lewati internal Next.js dan asset statis. Sisanya diperiksa proxy.
"/((?!_next/static|_next/image|favicon.ico|images/|.*\\.(?:png|jpg|jpeg|svg|webp|ico|css|js|map|txt|xml)$).*)",
],
};
+2 -2
View File
@@ -10,8 +10,8 @@ export const organizerRepo = {
return prisma.organizerVerification.findUnique({ where: { id } });
},
async findByNik(nik: string) {
return prisma.organizerVerification.findUnique({ where: { nik } });
async findByNikHash(nikHash: string) {
return prisma.organizerVerification.findUnique({ where: { nikHash } });
},
async upsertForUser(
+7
View File
@@ -27,4 +27,11 @@ export const userRepo = {
async create(data: Prisma.UserCreateInput) {
return prisma.user.create({ data });
},
async markAcceptedTerms(id: string) {
return prisma.user.update({
where: { id },
data: { acceptedTermsAndPrivacy: true, acceptedAt: new Date() },
});
},
};
+14 -6
View File
@@ -1,12 +1,13 @@
import { organizerRepo } from "@/server/repositories/organizer.repo";
import { decryptString, encryptString, hmacHex } from "@/lib/crypto";
type SubmitInput = {
fullName: string;
nik: string;
birthDate: Date;
address: string;
ktpImageUrl: string;
selfieUrl: string;
ktpImageKey: string;
selfieKey: string;
bankName: string;
bankAccountNumber: string;
bankAccountName: string;
@@ -22,18 +23,20 @@ export const organizerService = {
throw new Error("Pengajuan kamu masih dalam proses review");
}
const dupNik = await organizerRepo.findByNik(data.nik);
const nikHash = hmacHex(data.nik);
const dupNik = await organizerRepo.findByNikHash(nikHash);
if (dupNik && dupNik.userId !== userId) {
throw new Error("NIK ini sudah dipakai akun lain");
}
return organizerRepo.upsertForUser(userId, {
fullName: data.fullName,
nik: data.nik,
nikEncrypted: encryptString(data.nik),
nikHash,
birthDate: data.birthDate,
address: data.address,
ktpImageUrl: data.ktpImageUrl,
selfieUrl: data.selfieUrl,
ktpImageKey: data.ktpImageKey,
selfieKey: data.selfieKey,
bankName: data.bankName,
bankAccountNumber: data.bankAccountNumber,
bankAccountName: data.bankAccountName,
@@ -74,4 +77,9 @@ export const organizerService = {
const v = await organizerRepo.findByUserId(userId);
return v?.status === "APPROVED";
},
/** Reveal NIK plaintext. Caller must enforce authorization (owner or admin). */
decryptNik(nikEncrypted: string): string {
return decryptString(nikEncrypted);
},
};
+2
View File
@@ -7,6 +7,7 @@ declare module "next-auth" {
name: string;
email: string;
image?: string | null;
acceptedTermsAndPrivacy: boolean;
};
}
}
@@ -14,5 +15,6 @@ declare module "next-auth" {
declare module "next-auth/jwt" {
interface JWT {
id: string;
acceptedTermsAndPrivacy?: boolean;
}
}