mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-21 18:42:00 +00:00
Enhance user verification and data validation features
- Updated `next.config.ts` to include remote image patterns for user verification. - Introduced `VerifyUserDetails` function in `lib/person.ts` to validate user data against national records. - Added `usePerson` hook for fetching national data based on ID card. - Enhanced `signup` and `signin` functions in `auth-actions.ts` to handle user verification status and send notifications for pending verifications. - Refactored `VerifyUser` function in `user-actions.ts` to incorporate national data validation. - Improved UI components in the user verification page to display both database and national information. - Updated `InputReadOnly` component to support customizable label classes for better styling. These changes improve the user verification process, ensuring data integrity and enhancing the overall user experience.
This commit is contained in:
parent
1a195d2307
commit
ff0eae6ec4
@ -2,11 +2,13 @@
|
||||
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import prisma from "@/lib/db";
|
||||
import VerifyUserDetails from "@/lib/person";
|
||||
import { signUpFormSchema } from "@/lib/schemas";
|
||||
import { headers } from "next/headers";
|
||||
// import type { User } from "@prisma/client";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { SendUserRejectionDetailSMS } from "./user-actions";
|
||||
const formSchema = z.object({
|
||||
phoneNumber: z
|
||||
.string()
|
||||
@ -41,6 +43,14 @@ export async function signin(previousState: ActionState, formData: FormData) {
|
||||
if (!userExists) {
|
||||
return redirect(`/signup?phone_number=${phoneNumber}`);
|
||||
}
|
||||
|
||||
if (!userExists?.verified)
|
||||
return {
|
||||
message:
|
||||
"Your account is on pending verification. Please wait for a response from admin or contact shihaam.",
|
||||
status: "error",
|
||||
};
|
||||
|
||||
await authClient.phoneNumber.sendOtp({
|
||||
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
|
||||
});
|
||||
@ -79,6 +89,7 @@ export async function signup(_actionState: ActionState, formData: FormData) {
|
||||
NUMBER_WITH_COUNTRY_CODE = `+960${parsedData.data.phone_number.split("-").join("")}`;
|
||||
}
|
||||
console.log({ NUMBER_WITH_COUNTRY_CODE });
|
||||
|
||||
const idCardExists = await prisma.user.findFirst({
|
||||
where: {
|
||||
id_card: parsedData.data.id_card,
|
||||
@ -120,14 +131,39 @@ export async function signup(_actionState: ActionState, formData: FormData) {
|
||||
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
|
||||
},
|
||||
});
|
||||
await authClient.phoneNumber.sendOtp({
|
||||
phoneNumber: newUser.phoneNumber,
|
||||
});
|
||||
const isValidPerson = await VerifyUserDetails({ user: newUser });
|
||||
|
||||
if (!isValidPerson) {
|
||||
await SendUserRejectionDetailSMS({
|
||||
details: `
|
||||
A new user has requested for verification.
|
||||
USER DETAILS:
|
||||
Name: ${parsedData.data.name}
|
||||
Address: ${parsedData.data.address}
|
||||
ID Card: ${parsedData.data.id_card}
|
||||
DOB: ${parsedData.data.dob}
|
||||
ACC No: ${parsedData.data.accNo}
|
||||
Verify the user with the folloiwing link: ${process.env.BETTER_AUTH_URL}/users/${newUser.id}/verify
|
||||
`,
|
||||
phoneNumber: process.env.ADMIN_PHONENUMBER ?? "",
|
||||
});
|
||||
return {
|
||||
message:
|
||||
"Your account has been requested for verification. Please wait for a response from admin.",
|
||||
payload: formData,
|
||||
db_error: "invalidPersonValidation",
|
||||
};
|
||||
}
|
||||
|
||||
if (isValidPerson) {
|
||||
await authClient.phoneNumber.sendOtp({
|
||||
phoneNumber: newUser.phoneNumber,
|
||||
});
|
||||
}
|
||||
redirect(
|
||||
`/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`,
|
||||
);
|
||||
|
||||
return { message: "Post created" };
|
||||
return { message: "User created successfully" };
|
||||
}
|
||||
|
||||
export const sendOtp = async (phoneNumber: string, code: string) => {
|
||||
|
@ -1,25 +1,4 @@
|
||||
"use server";
|
||||
// const raw = {
|
||||
// group_settings_id: "",
|
||||
// address1: "",
|
||||
// city: "F",
|
||||
// state: "Dharanboodhoo",
|
||||
// postal_code: "12040",
|
||||
// country_id: "462",
|
||||
// address2: "Skyvilla",
|
||||
// contacts: [
|
||||
// {
|
||||
// first_name: "Abdulla",
|
||||
// last_name: "Aidhaan",
|
||||
// email: "",
|
||||
// phone: "778-0588",
|
||||
// send_email: false,
|
||||
// custom_value1: "1971-02-24",
|
||||
// custom_value2: "A265117",
|
||||
// custom_value3: "",
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
type CreateClientProps = {
|
||||
group_settings_id: string;
|
||||
|
@ -1,6 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import usePerson from "@/hooks/use-person";
|
||||
import prisma from "@/lib/db";
|
||||
import VerifyUserDetails from "@/lib/person";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { CreateClient } from "./ninja/client";
|
||||
@ -18,33 +20,42 @@ export async function VerifyUser(userId: string) {
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
verified: true,
|
||||
},
|
||||
});
|
||||
const ninjaClient = await CreateClient({
|
||||
group_settings_id: "",
|
||||
address1: "",
|
||||
city: user.atoll?.name || "",
|
||||
state: user.island?.name || "",
|
||||
postal_code: "",
|
||||
country_id: "462",
|
||||
address2: user.address || "",
|
||||
contacts: {
|
||||
first_name: user.name?.split(" ")[0] || "",
|
||||
last_name: user.name?.split(" ")[1] || "",
|
||||
email: user.email || "",
|
||||
phone: user.phoneNumber || "",
|
||||
send_email: false,
|
||||
custom_value1: user.dob?.toISOString().split("T")[0] || "",
|
||||
custom_value2: user.id_card || "",
|
||||
custom_value3: "",
|
||||
},
|
||||
});
|
||||
const isValidPerson = await VerifyUserDetails({ user });
|
||||
|
||||
if (!isValidPerson)
|
||||
throw new Error("The user details does not match national data.");
|
||||
|
||||
if (isValidPerson) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
verified: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ninjaClient = await CreateClient({
|
||||
group_settings_id: "",
|
||||
address1: "",
|
||||
city: user.atoll?.name || "",
|
||||
state: user.island?.name || "",
|
||||
postal_code: "",
|
||||
country_id: "462",
|
||||
address2: user.address || "",
|
||||
contacts: {
|
||||
first_name: user.name?.split(" ")[0] || "",
|
||||
last_name: user.name?.split(" ")[1] || "",
|
||||
email: user.email || "",
|
||||
phone: user.phoneNumber || "",
|
||||
send_email: false,
|
||||
custom_value1: user.dob?.toISOString().split("T")[0] || "",
|
||||
custom_value2: user.id_card || "",
|
||||
custom_value3: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
revalidatePath("/users");
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,11 @@ import InputReadOnly from '@/components/input-read-only';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import UserRejectDialog from '@/components/user/user-reject-dialog';
|
||||
import { UserVerifyDialog } from '@/components/user/user-verify-dialog';
|
||||
import usePerson from '@/hooks/use-person';
|
||||
|
||||
import prisma from '@/lib/db';
|
||||
import type { TNationalPerson } from '@/lib/types';
|
||||
import Image from 'next/image';
|
||||
import React from 'react'
|
||||
|
||||
export default async function VerifyUserPage({
|
||||
@ -18,7 +21,16 @@ export default async function VerifyUserPage({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
island: {
|
||||
include: {
|
||||
atoll: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const nationalData = await usePerson({ idCard: dbUser?.id_card ?? "" })
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4'>
|
||||
@ -35,30 +47,45 @@ export default async function VerifyUserPage({
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start'>
|
||||
<div id="database-information">
|
||||
<h4 className='p-2 rounded font-semibold'>Database Information</h4>
|
||||
<div className='shadow p-2 rounded-md space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2'>
|
||||
<InputReadOnly label="Name" value={dbUser?.name ?? ""} />
|
||||
<InputReadOnly label="ID Card" value={dbUser?.id_card ?? ""} />
|
||||
<InputReadOnly label="Address" value={dbUser?.address ?? ""} />
|
||||
<InputReadOnly label="DOB" value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
|
||||
<div className='shadow p-2 rounded-lg title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2'>
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="ID Card" value={dbUser?.id_card ?? ""} />
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="Name" value={dbUser?.name ?? ""} />
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="House Name" value={dbUser?.address ?? ""} />
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="Island" value={dbUser?.island?.name ?? ""} />
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="Atoll" value={dbUser?.island?.atoll.name ?? ""} />
|
||||
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="DOB" value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})} />
|
||||
<InputReadOnly label="Phone Number" value={dbUser?.phoneNumber ?? ""} />
|
||||
<InputReadOnly labelClassName='text-sarLinkOrange' label="Phone Number" value={dbUser?.phoneNumber ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
<div id="national-information">
|
||||
<h4 className='p-2 rounded font-semibold'>National Information</h4>
|
||||
<div className='shadow p-2 rounded-md space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2'>
|
||||
<InputReadOnly label="Name" value={dbUser?.name ?? ""} />
|
||||
<InputReadOnly label="ID Card" value={dbUser?.id_card ?? ""} />
|
||||
<InputReadOnly label="Address" value={dbUser?.address ?? ""} />
|
||||
<InputReadOnly label="DOB" value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
|
||||
<div className='shadow p-2 rounded-md title-bg space-y-1 my-2 grid grid-cols-1 md:grid-cols-2 gap-2'>
|
||||
<InputReadOnly labelClassName='text-green-500' label="ID Card" value={nationalData?.nic ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="Name" value={nationalData?.name_en ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="House Name" value={nationalData?.house_name_en ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="Island" value={nationalData?.island_name_en ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="Atoll" value={nationalData?.atoll_en ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="DOB" value={new Date(nationalData?.dob ?? "").toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})} />
|
||||
<InputReadOnly label="Phone Number" value={dbUser?.phoneNumber ?? ""} />
|
||||
<InputReadOnly labelClassName='text-green-500' label="Phone Number" value={nationalData?.primary_contact ?? ""} />
|
||||
<div className='flex flex-col col-span-2 items-center justify-center'>
|
||||
<Image
|
||||
src={nationalData.image_url}
|
||||
height={100}
|
||||
width={100}
|
||||
className='object-fit aspect-square rounded-full'
|
||||
alt='id photo'
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,6 @@ import type { Island, Prisma } from "@prisma/client";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useActionState } from "react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
@ -31,7 +30,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
|
||||
const [islands, setIslands] = React.useState<Island[]>();
|
||||
|
||||
const [actionState, action, isPending] = useActionState(signup, {
|
||||
const [actionState, action, isPending] = React.useActionState(signup, {
|
||||
message: "",
|
||||
});
|
||||
|
||||
@ -45,6 +44,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
const phoneNumberFromUrl = params.get("phone_number");
|
||||
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
action={action}
|
||||
@ -59,17 +59,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
<Input
|
||||
className={cn(
|
||||
"text-base",
|
||||
actionState.errors?.fieldErrors.name && "border-2 border-red-500",
|
||||
actionState?.errors?.fieldErrors.name && "border-2 border-red-500",
|
||||
)}
|
||||
name="name"
|
||||
type="text"
|
||||
disabled={isPending}
|
||||
defaultValue={(actionState.payload?.get("name") || "") as string}
|
||||
defaultValue={(actionState?.payload?.get("name") || "") as string}
|
||||
placeholder="Full Name"
|
||||
/>
|
||||
{actionState.errors?.fieldErrors.name && (
|
||||
{actionState?.errors?.fieldErrors.name && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors.name}
|
||||
{actionState?.errors?.fieldErrors.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -82,22 +82,22 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
type="text"
|
||||
maxLength={7}
|
||||
disabled={isPending}
|
||||
defaultValue={(actionState.payload?.get("id_card") || "") as string}
|
||||
defaultValue={(actionState?.payload?.get("id_card") || "") as string}
|
||||
className={cn(
|
||||
"text-base",
|
||||
actionState.errors?.fieldErrors?.id_card &&
|
||||
actionState?.errors?.fieldErrors?.id_card &&
|
||||
"border-2 border-red-500",
|
||||
)}
|
||||
placeholder="ID Card"
|
||||
/>
|
||||
{actionState?.errors?.fieldErrors?.id_card?.[0] && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors.fieldErrors.id_card[0]}
|
||||
{actionState?.errors.fieldErrors.id_card[0]}
|
||||
</span>
|
||||
)}
|
||||
{actionState.db_error === "id_card" && (
|
||||
{actionState?.db_error === "id_card" && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.message}
|
||||
{actionState?.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -128,9 +128,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
{actionState.errors?.fieldErrors?.atoll_id && (
|
||||
{actionState?.errors?.fieldErrors?.atoll_id && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.atoll_id}
|
||||
{actionState?.errors?.fieldErrors?.atoll_id}
|
||||
</span>
|
||||
)}
|
||||
</Select>
|
||||
@ -153,9 +153,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
{actionState.errors?.fieldErrors?.island_id && (
|
||||
{actionState?.errors?.fieldErrors?.island_id && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.island_id}
|
||||
{actionState?.errors?.fieldErrors?.island_id}
|
||||
</span>
|
||||
)}
|
||||
</Select>
|
||||
@ -169,20 +169,20 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
<Input
|
||||
className={cn(
|
||||
"text-base",
|
||||
actionState.errors?.fieldErrors?.address &&
|
||||
actionState?.errors?.fieldErrors?.address &&
|
||||
"border-2 border-red-500",
|
||||
)}
|
||||
disabled={isPending}
|
||||
name="address"
|
||||
defaultValue={
|
||||
(actionState.payload?.get("address") || "") as string
|
||||
(actionState?.payload?.get("address") || "") as string
|
||||
}
|
||||
type="text"
|
||||
placeholder="Address"
|
||||
/>
|
||||
{actionState.errors?.fieldErrors?.address && (
|
||||
{actionState?.errors?.fieldErrors?.address && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.address}
|
||||
{actionState?.errors?.fieldErrors?.address}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -194,17 +194,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
<Input
|
||||
className={cn(
|
||||
"text-base",
|
||||
actionState.errors?.fieldErrors?.dob && "border-2 border-red-500",
|
||||
actionState?.errors?.fieldErrors?.dob && "border-2 border-red-500",
|
||||
)}
|
||||
name="dob"
|
||||
disabled={isPending}
|
||||
defaultValue={(actionState.payload?.get("dob") || "") as string}
|
||||
defaultValue={(actionState?.payload?.get("dob") || "") as string}
|
||||
type="date"
|
||||
placeholder="Date of birth"
|
||||
/>
|
||||
{actionState.errors?.fieldErrors?.dob && (
|
||||
{actionState?.errors?.fieldErrors?.dob && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.dob}
|
||||
{actionState?.errors?.fieldErrors?.dob}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -216,17 +216,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
<Input
|
||||
className={cn(
|
||||
"text-base",
|
||||
actionState.errors?.fieldErrors.accNo && "border-2 border-red-500",
|
||||
actionState?.errors?.fieldErrors.accNo && "border-2 border-red-500",
|
||||
)}
|
||||
name="accNo"
|
||||
type="number"
|
||||
disabled={isPending}
|
||||
defaultValue={(actionState.payload?.get("accNo") || "") as string}
|
||||
defaultValue={(actionState?.payload?.get("accNo") || "") as string}
|
||||
placeholder="Account no"
|
||||
/>
|
||||
{actionState.errors?.fieldErrors.accNo && (
|
||||
{actionState?.errors?.fieldErrors.accNo && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors.accNo}
|
||||
{actionState?.errors?.fieldErrors.accNo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -241,7 +241,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
disabled={isPending}
|
||||
className={cn(
|
||||
!phoneNumberFromUrl &&
|
||||
actionState.errors?.fieldErrors?.phone_number &&
|
||||
actionState?.errors?.fieldErrors?.phone_number &&
|
||||
"border-2 border-red-500 rounded-md",
|
||||
)}
|
||||
defaultValue={NUMBER_WITHOUT_DASH ?? ""}
|
||||
@ -251,19 +251,19 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
</div>
|
||||
{actionState?.errors?.fieldErrors?.phone_number?.[0] && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors.fieldErrors.phone_number[0]}
|
||||
{actionState?.errors.fieldErrors.phone_number[0]}
|
||||
</span>
|
||||
)}
|
||||
{actionState.db_error === "phone_number" && (
|
||||
{actionState?.db_error === "phone_number" && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.message}
|
||||
{actionState?.message}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 items-start justify-start py-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={(actionState.payload?.get("terms") || "") as string === 'on'}
|
||||
defaultChecked={(actionState?.payload?.get("terms") || "") as string === 'on'}
|
||||
name="terms" id="terms" />
|
||||
<label
|
||||
htmlFor="terms"
|
||||
@ -277,16 +277,16 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
{actionState.errors?.fieldErrors?.terms && (
|
||||
{actionState?.errors?.fieldErrors?.terms && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.terms}
|
||||
{actionState?.errors?.fieldErrors?.terms}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-2 items-center">
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={(actionState.payload?.get("policy") || "") as string === 'on'}
|
||||
defaultChecked={(actionState?.payload?.get("policy") || "") as string === 'on'}
|
||||
name="policy" id="terms" />
|
||||
<label
|
||||
htmlFor="terms"
|
||||
@ -300,9 +300,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
{actionState.errors?.fieldErrors?.policy && (
|
||||
{actionState?.errors?.fieldErrors?.policy && (
|
||||
<span className="text-sm inline-block text-red-500">
|
||||
{actionState.errors?.fieldErrors?.policy}
|
||||
{actionState?.errors?.fieldErrors?.policy}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import React from 'react'
|
||||
|
||||
export default function InputReadOnly({ label, value }: { label: string, value?: string }) {
|
||||
export default function InputReadOnly({ label, value, labelClassName, className }: { label: string, value: string, labelClassName?: string, className?: string }) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 [&:has(input:is(:disabled))_*]:pointer-events-none">
|
||||
<label htmlFor="input-33" className="block px-3 pt-2 text-xs font-medium text-foreground">
|
||||
<div className={cn("relative rounded-lg border border-input bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-80 [&:has(input:is(:disabled))_*]:pointer-events-none", className)}>
|
||||
<label htmlFor="input-33" className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
|
@ -61,9 +61,9 @@ export function UserVerifyDialog({ user }: { user: User }) {
|
||||
setDisabled(false);
|
||||
return "User Verified!";
|
||||
},
|
||||
error: () => {
|
||||
error: (error: Error) => {
|
||||
setDisabled(false);
|
||||
return "Something went wrong";
|
||||
return error.message || "Something went wrong";
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
16
hooks/use-person.ts
Normal file
16
hooks/use-person.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { TNationalPerson } from "@/lib/types";
|
||||
|
||||
export default async function usePerson({
|
||||
idCard,
|
||||
}: { idCard: string }): Promise<TNationalPerson> {
|
||||
const nationalInformation = await fetch(
|
||||
`${process.env.PERSON_VERIFY_API_BASE}/api/person/${idCard}`,
|
||||
{
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
},
|
||||
);
|
||||
const nationalData = (await nationalInformation.json()) as TNationalPerson;
|
||||
return nationalData;
|
||||
}
|
21
lib/person.ts
Normal file
21
lib/person.ts
Normal file
@ -0,0 +1,21 @@
|
||||
"use server";
|
||||
import usePerson from "@/hooks/use-person";
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
export default async function VerifyUserDetails({ user }: { user: User }) {
|
||||
const phoneNumber = user.phoneNumber.slice(4);
|
||||
const nationalData = await usePerson({ idCard: user.id_card ?? "" });
|
||||
const dob = new Date(nationalData.dob);
|
||||
const age = new Date().getFullYear() - dob.getFullYear();
|
||||
|
||||
if (
|
||||
user.id_card === nationalData.nic &&
|
||||
user.name === nationalData.name_en &&
|
||||
user.address === nationalData.house_name_en &&
|
||||
phoneNumber === nationalData.primary_contact &&
|
||||
age >= 18
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
20
lib/types.ts
20
lib/types.ts
@ -48,3 +48,23 @@ export interface OmadaResponse {
|
||||
data: GroupProfile[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TNationalPerson {
|
||||
nic: string;
|
||||
name: string;
|
||||
name_en: string;
|
||||
dob: string;
|
||||
gender: "M" | "F"; // Assuming gender can only be Male or Female
|
||||
house_name: string;
|
||||
house_name_en: string;
|
||||
island_name: string;
|
||||
island_name_en: string;
|
||||
atoll: string;
|
||||
atoll_en: string;
|
||||
constituency: string;
|
||||
district_en: string | null;
|
||||
block_no: string | null;
|
||||
email: string | null;
|
||||
primary_contact: string;
|
||||
image_url: string;
|
||||
}
|
||||
|
@ -1,8 +1,19 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "verifypersonapi.baraveli.dev",
|
||||
pathname: "/images/**",
|
||||
search: "",
|
||||
port: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
@ -7,36 +7,44 @@ const DEFAULT_ISLANDS = ["Dharanboodhoo", "Feeali", "Nilandhoo", "Magoodhoo"];
|
||||
async function main() {
|
||||
await prisma.user.upsert({
|
||||
where: {
|
||||
phoneNumber: "+9607780588",
|
||||
phoneNumber: process.env.ADMIN_PHONENUMBER,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
name: "Admin Admin",
|
||||
email: "admin@example.com",
|
||||
email: process.env.ADMIN_EMAIL,
|
||||
emailVerified: true,
|
||||
verified: true,
|
||||
address: "Sky villa",
|
||||
id_card: "A265117",
|
||||
id_card: process.env.ADMIN_IDCARD,
|
||||
dob: new Date("1990-01-01"),
|
||||
phoneNumber: "+9607780588",
|
||||
phoneNumber: process.env.ADMIN_PHONENUMBER ?? "",
|
||||
phoneNumberVerified: true,
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
|
||||
const FAAFU_ATOLL = await prisma.atoll.create({
|
||||
data: {
|
||||
let FAAFU_ATOLL_ID = "";
|
||||
const atollExists = await prisma.atoll.findFirst({
|
||||
where: {
|
||||
name: "F",
|
||||
},
|
||||
});
|
||||
if (!atollExists) {
|
||||
const NEW_ATOLL = await prisma.atoll.create({
|
||||
data: {
|
||||
name: "F",
|
||||
},
|
||||
});
|
||||
FAAFU_ATOLL_ID = NEW_ATOLL.id;
|
||||
|
||||
const islands = DEFAULT_ISLANDS.map((name) => ({
|
||||
name,
|
||||
atollId: FAAFU_ATOLL.id,
|
||||
}));
|
||||
await prisma.island.createMany({
|
||||
data: islands,
|
||||
});
|
||||
const islands = DEFAULT_ISLANDS.map((name) => ({
|
||||
name,
|
||||
atollId: FAAFU_ATOLL_ID,
|
||||
}));
|
||||
await prisma.island.createMany({
|
||||
data: islands,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
Loading…
x
Reference in New Issue
Block a user