refactor: migrate authentication and signup flow to use external API and improve type safety
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m58s

This commit is contained in:
i701 2025-01-24 11:42:38 +05:00
parent 8ffabb1fcb
commit 0fd269df31
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
9 changed files with 96 additions and 73 deletions

View File

@ -4,8 +4,8 @@ import { authClient } from "@/lib/auth-client";
import prisma from "@/lib/db";
import { VerifyUserDetails } from "@/lib/person";
import { signUpFormSchema } from "@/lib/schemas";
import { phoneNumber } from "better-auth/plugins";
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";
@ -33,14 +33,23 @@ export async function signin(previousState: ActionState, formData: FormData) {
status: "error",
};
}
const NUMBER_WITH_COUNTRY_CODE: string = `+960${phoneNumber.split("-").join("")}`;
const userExists = await prisma.user.findUnique({
where: {
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`;
console.log(FORMATTED_MOBILE_NUMBER);
const userExistsResponse = await fetch(
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
mobile: FORMATTED_MOBILE_NUMBER,
}),
},
});
if (!userExists) {
);
const userExists = await userExistsResponse.json();
console.log(userExists.non_field_errors);
if (userExists?.non_field_errors) {
return redirect(`/signup?phone_number=${phoneNumber}`);
}
@ -51,12 +60,10 @@ export async function signin(previousState: ActionState, formData: FormData) {
status: "error",
};
await authClient.phoneNumber.sendOtp({
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
});
redirect(
`/verify-otp?phone_number=${encodeURIComponent(NUMBER_WITH_COUNTRY_CODE)}`,
);
// await authClient.phoneNumber.sendOtp({
// phoneNumber: NUMBER_WITH_COUNTRY_CODE,
// });
redirect(`/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`);
}
type ActionState = {
@ -69,11 +76,8 @@ export async function signup(_actionState: ActionState, formData: FormData) {
const parsedData = signUpFormSchema.safeParse(data);
// get phone number from /signup?phone_number=999-1231
const headersList = await headers();
const referer = headersList.get("referer");
const number = referer?.split("?")[1]?.split("=")[1];
let NUMBER_WITH_COUNTRY_CODE: string;
console.log(data);
console.log("DATA ON SERVER SIDE", data);
if (!parsedData.success) {
return {
@ -83,13 +87,6 @@ export async function signup(_actionState: ActionState, formData: FormData) {
};
}
if (number) {
NUMBER_WITH_COUNTRY_CODE = `+960${number.split("-").join("")}`;
} else {
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,
@ -106,7 +103,7 @@ export async function signup(_actionState: ActionState, formData: FormData) {
const phoneNumberExists = await prisma.user.findFirst({
where: {
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
phoneNumber: parsedData.data.phone_number,
},
});
@ -128,7 +125,7 @@ export async function signup(_actionState: ActionState, formData: FormData) {
dob: new Date(parsedData.data.dob),
role: "USER",
accNo: parsedData.data.accNo,
phoneNumber: NUMBER_WITH_COUNTRY_CODE,
phoneNumber: parsedData.data.phone_number,
},
});
const isValidPerson = await VerifyUserDetails({ user: newUser });

View File

@ -6,12 +6,7 @@ import { redirect } from "next/navigation";
import React from "react";
export default async function LoginPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (session) {
return redirect("/devices");
}
return (
<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">

View File

@ -1,44 +1,33 @@
import SignUpForm from "@/components/auth/signup-form";
import { auth } from "@/lib/auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import { getAtollsWithIslands } from "@/queries/atoll";
import Image from "next/image";
import { redirect } from "next/navigation";
import React from "react";
export default async function SignupPage({
searchParams,
}: {
searchParams: Promise<{ phone_number: string }>;
}) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (session) {
return redirect("/devices");
}
const atolls = await getAtollsWithIslands();
console.log(atolls.data);
const phone_number = (await searchParams).phone_number;
if (!phone_number) {
return redirect("/login");
}
const atolls = await prisma.atoll.findMany({
include: {
islands: true,
},
});
return (
<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<Image priority alt="Sar Link Logo" src="/logo.png" width={100} height={100} style={{ width: "auto", height: "auto" }} />
<div className="mt-4 flex flex-col items-center justify-center">
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">
Pay for your devices and track your bills.
</p>
</div>
<SignUpForm atolls={atolls} />
<SignUpForm atolls={atolls.data} />
</div>
</div>
);

View File

@ -1,9 +1,14 @@
import { ApplicationLayout } from "@/components/auth/application-layout";
import QueryProvider from "@/components/query-provider";
export default function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <ApplicationLayout>{children}</ApplicationLayout>;
return <ApplicationLayout>
<QueryProvider>
{children}
</QueryProvider>
</ApplicationLayout>;
}

View File

@ -6,7 +6,6 @@ import { Barlow } from "next/font/google";
import NextTopLoader from "nextjs-toploader";
import { Toaster } from "sonner";
import "./globals.css";
import QueryProvider from "@/components/query-provider";
const barlow = Barlow({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
@ -35,7 +34,7 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<QueryProvider>{children}</QueryProvider>
{children}
</ThemeProvider>
</Provider>
</body>

View File

@ -5,7 +5,6 @@ import Link from "next/link";
import { signup } from "@/actions/auth-actions";
import { cn } from "@/lib/utils";
import type { Island, Prisma } from "@prisma/client";
import { Loader2 } from "lucide-react";
import { useSearchParams } from "next/navigation";
import * as React from "react";
@ -19,16 +18,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { Atoll } from "@/lib/backend-types";
type AtollWithIslands = Prisma.AtollGetPayload<{
include: {
islands: true;
};
}>;
export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
const [islands, setIslands] = React.useState<Island[]>();
export default function SignUpForm({ atolls }: { atolls: Atoll[] }) {
const [atoll, setAtoll] = React.useState<Atoll>();
const [actionState, action, isPending] = React.useActionState(signup, {
message: "",
@ -36,7 +30,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
React.useEffect(() => {
setIslands(atoll?.islands);
console.log(atoll)
}, [atoll]);
@ -122,11 +116,11 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
<Select
disabled={isPending}
onValueChange={(v) => {
setAtoll(atolls.find((atoll) => atoll.id === v));
setIslands([]);
console.log({ v })
setAtoll(atolls.find((atoll) => atoll.id === Number.parseInt(v)));
}}
name="atoll_id"
value={atoll?.id}
value={atoll?.id?.toString() ?? ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select atoll" />
@ -135,7 +129,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
<SelectGroup>
<SelectLabel>Atolls</SelectLabel>
{atolls.map((atoll) => (
<SelectItem key={atoll.id} value={atoll.id}>
<SelectItem key={atoll.id} value={atoll.id.toString()}>
{atoll.name}
</SelectItem>
))}
@ -159,8 +153,8 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
<SelectContent>
<SelectGroup>
<SelectLabel>Islands</SelectLabel>
{islands?.map((island) => (
<SelectItem key={island.id} value={island.id}>
{atoll?.islands?.map((island) => (
<SelectItem key={island.id} value={island.id.toString()}>
{island.name}
</SelectItem>
))}
@ -333,4 +327,4 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
</div>
</form>
);
}
}

31
lib/backend-types.ts Normal file
View File

@ -0,0 +1,31 @@
export interface Links {
next_page: string | null;
previous_page: string | null;
}
export interface Meta {
total: number;
per_page: number;
current_page: number;
last_page: number;
}
export interface DataResponse<T> {
meta: Meta;
links: Links;
data: T[];
}
export interface Atoll {
id: number;
islands: Island[];
name: string;
createdAt: string;
updatedAt: string;
}
export interface Island {
id: number;
name: string;
createdAt: string;
updatedAt: string;
}

View File

@ -5,10 +5,10 @@ export const signUpFormSchema = z.object({
.string()
.min(2, { message: "ID Card is required" })
.regex(/^[A][0-9]{6}$/, "Please enter a valid ID Card number."),
atoll_id: z.string().min(2, { message: "Atoll is required." }),
atoll_id: z.string().min(1, { message: "Atoll is required." }),
island_id: z
.string({ required_error: "Island is required." })
.min(2, { message: "Island is required." }),
.min(1, { message: "Island is required." }),
address: z.string().min(2, { message: "address is required." }),
dob: z.coerce.date({ message: "Date of birth is required." }),
phone_number: z
@ -26,5 +26,8 @@ export const signUpFormSchema = z.object({
required_error: "You must accept the privacy policy",
})
.transform((val) => val === "on"),
accNo: z.string().min(2, { message: "Account number is required." }),
accNo: z
.string()
.min(2, { message: "Account number is required." })
.regex(/^(7\d{12}|9\d{16})$/, "Please enter a valid account number"),
});

10
queries/atoll.ts Normal file
View File

@ -0,0 +1,10 @@
"use server";
import type { Atoll, DataResponse } from "@/lib/backend-types";
export async function getAtollsWithIslands(): Promise<DataResponse<Atoll>> {
const response = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/atolls`,
);
return response.json();
}