mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-22 09:22:01 +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 { authClient } from "@/lib/auth-client";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
|
import VerifyUserDetails from "@/lib/person";
|
||||||
import { signUpFormSchema } from "@/lib/schemas";
|
import { signUpFormSchema } from "@/lib/schemas";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
// import type { User } from "@prisma/client";
|
// import type { User } from "@prisma/client";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { SendUserRejectionDetailSMS } from "./user-actions";
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
phoneNumber: z
|
phoneNumber: z
|
||||||
.string()
|
.string()
|
||||||
@ -41,6 +43,14 @@ export async function signin(previousState: ActionState, formData: FormData) {
|
|||||||
if (!userExists) {
|
if (!userExists) {
|
||||||
return redirect(`/signup?phone_number=${phoneNumber}`);
|
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({
|
await authClient.phoneNumber.sendOtp({
|
||||||
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
|
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("")}`;
|
NUMBER_WITH_COUNTRY_CODE = `+960${parsedData.data.phone_number.split("-").join("")}`;
|
||||||
}
|
}
|
||||||
console.log({ NUMBER_WITH_COUNTRY_CODE });
|
console.log({ NUMBER_WITH_COUNTRY_CODE });
|
||||||
|
|
||||||
const idCardExists = await prisma.user.findFirst({
|
const idCardExists = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id_card: parsedData.data.id_card,
|
id_card: parsedData.data.id_card,
|
||||||
@ -120,14 +131,39 @@ export async function signup(_actionState: ActionState, formData: FormData) {
|
|||||||
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
|
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await authClient.phoneNumber.sendOtp({
|
const isValidPerson = await VerifyUserDetails({ user: newUser });
|
||||||
phoneNumber: newUser.phoneNumber,
|
|
||||||
});
|
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(
|
redirect(
|
||||||
`/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`,
|
`/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`,
|
||||||
);
|
);
|
||||||
|
return { message: "User created successfully" };
|
||||||
return { message: "Post created" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendOtp = async (phoneNumber: string, code: string) => {
|
export const sendOtp = async (phoneNumber: string, code: string) => {
|
||||||
|
@ -1,25 +1,4 @@
|
|||||||
"use server";
|
"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 = {
|
type CreateClientProps = {
|
||||||
group_settings_id: string;
|
group_settings_id: string;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import usePerson from "@/hooks/use-person";
|
||||||
import prisma from "@/lib/db";
|
import prisma from "@/lib/db";
|
||||||
|
import VerifyUserDetails from "@/lib/person";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { CreateClient } from "./ninja/client";
|
import { CreateClient } from "./ninja/client";
|
||||||
@ -18,33 +20,42 @@ export async function VerifyUser(userId: string) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("User not found");
|
throw new Error("User not found");
|
||||||
}
|
}
|
||||||
await prisma.user.update({
|
const isValidPerson = await VerifyUserDetails({ user });
|
||||||
where: {
|
|
||||||
id: userId,
|
if (!isValidPerson)
|
||||||
},
|
throw new Error("The user details does not match national data.");
|
||||||
data: {
|
|
||||||
verified: true,
|
if (isValidPerson) {
|
||||||
},
|
await prisma.user.update({
|
||||||
});
|
where: {
|
||||||
const ninjaClient = await CreateClient({
|
id: userId,
|
||||||
group_settings_id: "",
|
},
|
||||||
address1: "",
|
data: {
|
||||||
city: user.atoll?.name || "",
|
verified: true,
|
||||||
state: user.island?.name || "",
|
},
|
||||||
postal_code: "",
|
});
|
||||||
country_id: "462",
|
|
||||||
address2: user.address || "",
|
const ninjaClient = await CreateClient({
|
||||||
contacts: {
|
group_settings_id: "",
|
||||||
first_name: user.name?.split(" ")[0] || "",
|
address1: "",
|
||||||
last_name: user.name?.split(" ")[1] || "",
|
city: user.atoll?.name || "",
|
||||||
email: user.email || "",
|
state: user.island?.name || "",
|
||||||
phone: user.phoneNumber || "",
|
postal_code: "",
|
||||||
send_email: false,
|
country_id: "462",
|
||||||
custom_value1: user.dob?.toISOString().split("T")[0] || "",
|
address2: user.address || "",
|
||||||
custom_value2: user.id_card || "",
|
contacts: {
|
||||||
custom_value3: "",
|
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");
|
revalidatePath("/users");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,11 @@ import InputReadOnly from '@/components/input-read-only';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import UserRejectDialog from '@/components/user/user-reject-dialog';
|
import UserRejectDialog from '@/components/user/user-reject-dialog';
|
||||||
import { UserVerifyDialog } from '@/components/user/user-verify-dialog';
|
import { UserVerifyDialog } from '@/components/user/user-verify-dialog';
|
||||||
|
import usePerson from '@/hooks/use-person';
|
||||||
|
|
||||||
import prisma from '@/lib/db';
|
import prisma from '@/lib/db';
|
||||||
|
import type { TNationalPerson } from '@/lib/types';
|
||||||
|
import Image from 'next/image';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export default async function VerifyUserPage({
|
export default async function VerifyUserPage({
|
||||||
@ -18,7 +21,16 @@ export default async function VerifyUserPage({
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
island: {
|
||||||
|
include: {
|
||||||
|
atoll: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const nationalData = await usePerson({ idCard: dbUser?.id_card ?? "" })
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='flex items-center justify-between text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4'>
|
<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 className='grid grid-cols-1 md:grid-cols-2 gap-4 items-start justify-start'>
|
||||||
<div id="database-information">
|
<div id="database-information">
|
||||||
<h4 className='p-2 rounded font-semibold'>Database Information</h4>
|
<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'>
|
<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 label="Name" value={dbUser?.name ?? ""} />
|
<InputReadOnly labelClassName='text-sarLinkOrange' label="ID Card" value={dbUser?.id_card ?? ""} />
|
||||||
<InputReadOnly label="ID Card" value={dbUser?.id_card ?? ""} />
|
<InputReadOnly labelClassName='text-sarLinkOrange' label="Name" value={dbUser?.name ?? ""} />
|
||||||
<InputReadOnly label="Address" value={dbUser?.address ?? ""} />
|
<InputReadOnly labelClassName='text-sarLinkOrange' label="House Name" value={dbUser?.address ?? ""} />
|
||||||
<InputReadOnly label="DOB" value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
|
<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",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})} />
|
})} />
|
||||||
<InputReadOnly label="Phone Number" value={dbUser?.phoneNumber ?? ""} />
|
<InputReadOnly labelClassName='text-sarLinkOrange' label="Phone Number" value={dbUser?.phoneNumber ?? ""} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="national-information">
|
<div id="national-information">
|
||||||
<h4 className='p-2 rounded font-semibold'>National Information</h4>
|
<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'>
|
<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 label="Name" value={dbUser?.name ?? ""} />
|
<InputReadOnly labelClassName='text-green-500' label="ID Card" value={nationalData?.nic ?? ""} />
|
||||||
<InputReadOnly label="ID Card" value={dbUser?.id_card ?? ""} />
|
<InputReadOnly labelClassName='text-green-500' label="Name" value={nationalData?.name_en ?? ""} />
|
||||||
<InputReadOnly label="Address" value={dbUser?.address ?? ""} />
|
<InputReadOnly labelClassName='text-green-500' label="House Name" value={nationalData?.house_name_en ?? ""} />
|
||||||
<InputReadOnly label="DOB" value={new Date(dbUser?.dob ?? "").toLocaleDateString("en-US", {
|
<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",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,6 @@ import type { Island, Prisma } from "@prisma/client";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useActionState } from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@ -31,7 +30,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
|
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
|
||||||
const [islands, setIslands] = React.useState<Island[]>();
|
const [islands, setIslands] = React.useState<Island[]>();
|
||||||
|
|
||||||
const [actionState, action, isPending] = useActionState(signup, {
|
const [actionState, action, isPending] = React.useActionState(signup, {
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,6 +44,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
const phoneNumberFromUrl = params.get("phone_number");
|
const phoneNumberFromUrl = params.get("phone_number");
|
||||||
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
|
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
action={action}
|
action={action}
|
||||||
@ -59,17 +59,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
<Input
|
<Input
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-base",
|
"text-base",
|
||||||
actionState.errors?.fieldErrors.name && "border-2 border-red-500",
|
actionState?.errors?.fieldErrors.name && "border-2 border-red-500",
|
||||||
)}
|
)}
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
defaultValue={(actionState.payload?.get("name") || "") as string}
|
defaultValue={(actionState?.payload?.get("name") || "") as string}
|
||||||
placeholder="Full Name"
|
placeholder="Full Name"
|
||||||
/>
|
/>
|
||||||
{actionState.errors?.fieldErrors.name && (
|
{actionState?.errors?.fieldErrors.name && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors.name}
|
{actionState?.errors?.fieldErrors.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -82,22 +82,22 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
type="text"
|
type="text"
|
||||||
maxLength={7}
|
maxLength={7}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
defaultValue={(actionState.payload?.get("id_card") || "") as string}
|
defaultValue={(actionState?.payload?.get("id_card") || "") as string}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-base",
|
"text-base",
|
||||||
actionState.errors?.fieldErrors?.id_card &&
|
actionState?.errors?.fieldErrors?.id_card &&
|
||||||
"border-2 border-red-500",
|
"border-2 border-red-500",
|
||||||
)}
|
)}
|
||||||
placeholder="ID Card"
|
placeholder="ID Card"
|
||||||
/>
|
/>
|
||||||
{actionState?.errors?.fieldErrors?.id_card?.[0] && (
|
{actionState?.errors?.fieldErrors?.id_card?.[0] && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors.fieldErrors.id_card[0]}
|
{actionState?.errors.fieldErrors.id_card[0]}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{actionState.db_error === "id_card" && (
|
{actionState?.db_error === "id_card" && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.message}
|
{actionState?.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -128,9 +128,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
{actionState.errors?.fieldErrors?.atoll_id && (
|
{actionState?.errors?.fieldErrors?.atoll_id && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.atoll_id}
|
{actionState?.errors?.fieldErrors?.atoll_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
@ -153,9 +153,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
{actionState.errors?.fieldErrors?.island_id && (
|
{actionState?.errors?.fieldErrors?.island_id && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.island_id}
|
{actionState?.errors?.fieldErrors?.island_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
@ -169,20 +169,20 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
<Input
|
<Input
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-base",
|
"text-base",
|
||||||
actionState.errors?.fieldErrors?.address &&
|
actionState?.errors?.fieldErrors?.address &&
|
||||||
"border-2 border-red-500",
|
"border-2 border-red-500",
|
||||||
)}
|
)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
name="address"
|
name="address"
|
||||||
defaultValue={
|
defaultValue={
|
||||||
(actionState.payload?.get("address") || "") as string
|
(actionState?.payload?.get("address") || "") as string
|
||||||
}
|
}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Address"
|
placeholder="Address"
|
||||||
/>
|
/>
|
||||||
{actionState.errors?.fieldErrors?.address && (
|
{actionState?.errors?.fieldErrors?.address && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.address}
|
{actionState?.errors?.fieldErrors?.address}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -194,17 +194,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
<Input
|
<Input
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-base",
|
"text-base",
|
||||||
actionState.errors?.fieldErrors?.dob && "border-2 border-red-500",
|
actionState?.errors?.fieldErrors?.dob && "border-2 border-red-500",
|
||||||
)}
|
)}
|
||||||
name="dob"
|
name="dob"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
defaultValue={(actionState.payload?.get("dob") || "") as string}
|
defaultValue={(actionState?.payload?.get("dob") || "") as string}
|
||||||
type="date"
|
type="date"
|
||||||
placeholder="Date of birth"
|
placeholder="Date of birth"
|
||||||
/>
|
/>
|
||||||
{actionState.errors?.fieldErrors?.dob && (
|
{actionState?.errors?.fieldErrors?.dob && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.dob}
|
{actionState?.errors?.fieldErrors?.dob}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -216,17 +216,17 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
<Input
|
<Input
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-base",
|
"text-base",
|
||||||
actionState.errors?.fieldErrors.accNo && "border-2 border-red-500",
|
actionState?.errors?.fieldErrors.accNo && "border-2 border-red-500",
|
||||||
)}
|
)}
|
||||||
name="accNo"
|
name="accNo"
|
||||||
type="number"
|
type="number"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
defaultValue={(actionState.payload?.get("accNo") || "") as string}
|
defaultValue={(actionState?.payload?.get("accNo") || "") as string}
|
||||||
placeholder="Account no"
|
placeholder="Account no"
|
||||||
/>
|
/>
|
||||||
{actionState.errors?.fieldErrors.accNo && (
|
{actionState?.errors?.fieldErrors.accNo && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors.accNo}
|
{actionState?.errors?.fieldErrors.accNo}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -241,7 +241,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className={cn(
|
className={cn(
|
||||||
!phoneNumberFromUrl &&
|
!phoneNumberFromUrl &&
|
||||||
actionState.errors?.fieldErrors?.phone_number &&
|
actionState?.errors?.fieldErrors?.phone_number &&
|
||||||
"border-2 border-red-500 rounded-md",
|
"border-2 border-red-500 rounded-md",
|
||||||
)}
|
)}
|
||||||
defaultValue={NUMBER_WITHOUT_DASH ?? ""}
|
defaultValue={NUMBER_WITHOUT_DASH ?? ""}
|
||||||
@ -251,19 +251,19 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
</div>
|
</div>
|
||||||
{actionState?.errors?.fieldErrors?.phone_number?.[0] && (
|
{actionState?.errors?.fieldErrors?.phone_number?.[0] && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors.fieldErrors.phone_number[0]}
|
{actionState?.errors.fieldErrors.phone_number[0]}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{actionState.db_error === "phone_number" && (
|
{actionState?.db_error === "phone_number" && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.message}
|
{actionState?.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 items-start justify-start py-2">
|
<div className="flex flex-col gap-2 items-start justify-start py-2">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={(actionState.payload?.get("terms") || "") as string === 'on'}
|
defaultChecked={(actionState?.payload?.get("terms") || "") as string === 'on'}
|
||||||
name="terms" id="terms" />
|
name="terms" id="terms" />
|
||||||
<label
|
<label
|
||||||
htmlFor="terms"
|
htmlFor="terms"
|
||||||
@ -277,16 +277,16 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{actionState.errors?.fieldErrors?.terms && (
|
{actionState?.errors?.fieldErrors?.terms && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.terms}
|
{actionState?.errors?.fieldErrors?.terms}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
defaultChecked={(actionState.payload?.get("policy") || "") as string === 'on'}
|
defaultChecked={(actionState?.payload?.get("policy") || "") as string === 'on'}
|
||||||
name="policy" id="terms" />
|
name="policy" id="terms" />
|
||||||
<label
|
<label
|
||||||
htmlFor="terms"
|
htmlFor="terms"
|
||||||
@ -300,9 +300,9 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{actionState.errors?.fieldErrors?.policy && (
|
{actionState?.errors?.fieldErrors?.policy && (
|
||||||
<span className="text-sm inline-block text-red-500">
|
<span className="text-sm inline-block text-red-500">
|
||||||
{actionState.errors?.fieldErrors?.policy}
|
{actionState?.errors?.fieldErrors?.policy}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
import React from 'react'
|
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 (
|
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">
|
<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="block px-3 pt-2 text-xs font-medium text-foreground">
|
<label htmlFor="input-33" className={cn("block px-3 pt-2 text-xs font-medium", labelClassName)}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
@ -61,9 +61,9 @@ export function UserVerifyDialog({ user }: { user: User }) {
|
|||||||
setDisabled(false);
|
setDisabled(false);
|
||||||
return "User Verified!";
|
return "User Verified!";
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (error: Error) => {
|
||||||
setDisabled(false);
|
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[];
|
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";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "standalone",
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "http",
|
||||||
|
hostname: "verifypersonapi.baraveli.dev",
|
||||||
|
pathname: "/images/**",
|
||||||
|
search: "",
|
||||||
|
port: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
@ -7,36 +7,44 @@ const DEFAULT_ISLANDS = ["Dharanboodhoo", "Feeali", "Nilandhoo", "Magoodhoo"];
|
|||||||
async function main() {
|
async function main() {
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: {
|
where: {
|
||||||
phoneNumber: "+9607780588",
|
phoneNumber: process.env.ADMIN_PHONENUMBER,
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
name: "Admin Admin",
|
name: "Admin Admin",
|
||||||
email: "admin@example.com",
|
email: process.env.ADMIN_EMAIL,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
address: "Sky villa",
|
address: "Sky villa",
|
||||||
id_card: "A265117",
|
id_card: process.env.ADMIN_IDCARD,
|
||||||
dob: new Date("1990-01-01"),
|
dob: new Date("1990-01-01"),
|
||||||
phoneNumber: "+9607780588",
|
phoneNumber: process.env.ADMIN_PHONENUMBER ?? "",
|
||||||
phoneNumberVerified: true,
|
phoneNumberVerified: true,
|
||||||
role: "ADMIN",
|
role: "ADMIN",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
let FAAFU_ATOLL_ID = "";
|
||||||
const FAAFU_ATOLL = await prisma.atoll.create({
|
const atollExists = await prisma.atoll.findFirst({
|
||||||
data: {
|
where: {
|
||||||
name: "F",
|
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) => ({
|
const islands = DEFAULT_ISLANDS.map((name) => ({
|
||||||
name,
|
name,
|
||||||
atollId: FAAFU_ATOLL.id,
|
atollId: FAAFU_ATOLL_ID,
|
||||||
}));
|
}));
|
||||||
await prisma.island.createMany({
|
await prisma.island.createMany({
|
||||||
data: islands,
|
data: islands,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user