diff --git a/actions/auth-actions.ts b/actions/auth-actions.ts index ddccefc..564de7b 100644 --- a/actions/auth-actions.ts +++ b/actions/auth-actions.ts @@ -1,11 +1,9 @@ "use server"; -import { VerifyUserDetails } from "@/lib/person"; import { signUpFormSchema } from "@/lib/schemas"; -import { headers } from "next/headers"; +import { checkIdOrPhone } from "@/queries/authentication"; import { redirect } from "next/navigation"; import { z } from "zod"; -import { SendUserRejectionDetailSMS } from "./user-actions"; const formSchema = z.object({ phoneNumber: z .string() @@ -84,33 +82,28 @@ export async function signup(_actionState: ActionState, formData: FormData) { }; } - // const idCardExists = await prisma.user.findFirst({ - // where: { - // id_card: parsedData.data.id_card, - // }, - // }); + const idCardExists = await checkIdOrPhone({ + id_card: parsedData.data.id_card, + }); + if (idCardExists.ok) { + return { + message: "ID card already exists.", + payload: formData, + db_error: "id_card", + }; + } - // if (idCardExists) { - // return { - // message: "ID card already exists.", - // payload: formData, - // db_error: "id_card", - // }; - // } + const phoneNumberExists = await checkIdOrPhone({ + phone_number: parsedData.data.phone_number, + }); - // const phoneNumberExists = await prisma.user.findFirst({ - // where: { - // phoneNumber: parsedData.data.phone_number, - // }, - // }); - - // if (phoneNumberExists) { - // return { - // message: "Phone number already exists.", - // payload: formData, - // db_error: "phone_number", - // }; - // } + if (phoneNumberExists.ok) { + return { + message: "Phone number already exists.", + payload: formData, + db_error: "phone_number", + }; + } // const newUser = await prisma.user.create({ // data: { @@ -144,12 +137,12 @@ export async function signup(_actionState: ActionState, formData: FormData) { // `, // 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", - // }; + // 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({ @@ -159,7 +152,7 @@ export async function signup(_actionState: ActionState, formData: FormData) { // redirect( // `/verify-otp?phone_number=${encodeURIComponent(newUser.phoneNumber)}`, // ); - // return { message: "User created successfully" }; + return { message: "User created successfully" }; } export const sendOtp = async (phoneNumber: string, code: string) => { diff --git a/app/(auth)/auth/signup/page.tsx b/app/(auth)/auth/signup/page.tsx index 05d89b0..8cf7cdb 100644 --- a/app/(auth)/auth/signup/page.tsx +++ b/app/(auth)/auth/signup/page.tsx @@ -1,4 +1,6 @@ import SignUpForm from "@/components/auth/signup-form"; +import type { ApiResponse, Atoll, Island } from "@/lib/backend-types"; +import { getAtolls } from "@/queries/islands"; import Image from "next/image"; import { redirect } from "next/navigation"; @@ -8,11 +10,10 @@ export default async function SignupPage({ searchParams: Promise<{ phone_number: string }>; }) { - const atolls = await getAtollsWithIslands(); - console.log(atolls.data); const phone_number = (await searchParams).phone_number; + console.log({ phone_number }) if (!phone_number) { - return redirect("/login"); + return redirect("/auth/login"); } @@ -26,7 +27,7 @@ export default async function SignupPage({ Pay for your devices and track your bills.

- + ); diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 2b55887..b64c312 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,5 +1,5 @@ import { ApplicationLayout } from "@/components/auth/application-layout"; -import QueryProvider from "@/components/query-provider"; +import QueryProvider from "@/providers/query-provider"; export default function DashboardLayout({ children, diff --git a/app/layout.tsx b/app/layout.tsx index a3327cd..e93b425 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from "@/components/theme-provider"; +import { ThemeProvider } from "@/providers/theme-provider"; import { Provider } from "jotai"; import type { Metadata } from "next"; @@ -6,6 +6,10 @@ import { Barlow } from "next/font/google"; import NextTopLoader from "nextjs-toploader"; import { Toaster } from "sonner"; import "./globals.css"; +import { AuthProvider } from "@/providers/AuthProvider"; +import QueryProvider from "@/providers/query-provider"; +import { getServerSession } from "next-auth"; +import { authOptions } from "./auth"; const barlow = Barlow({ subsets: ["latin"], weight: ["100", "300", "400", "500", "600", "700", "800", "900"], @@ -17,27 +21,33 @@ export const metadata: Metadata = { description: "Sarlink Portal", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const session = await getServerSession(authOptions); return ( - - - - - {children} - - + + + + + + + {children} + + + + + ); } diff --git a/components/auth/signup-form.tsx b/components/auth/signup-form.tsx index d438751..38b8910 100644 --- a/components/auth/signup-form.tsx +++ b/components/auth/signup-form.tsx @@ -18,10 +18,20 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import type { Atoll } from "@/lib/backend-types"; +import type { ApiResponse, Atoll } from "@/lib/backend-types"; +import { getAtolls } from "@/queries/islands"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; -export default function SignUpForm({ atolls }: { atolls: Atoll[] }) { +export default function SignUpForm() { + const { data: atolls, isFetching } = useQuery>({ + queryKey: ["ATOLLS"], + queryFn: () => + getAtolls(), + placeholderData: keepPreviousData, + staleTime: 1, + }); + const [atoll, setAtoll] = React.useState(); const [actionState, action, isPending] = React.useActionState(signup, { @@ -39,7 +49,7 @@ export default function SignUpForm({ atolls }: { atolls: Atoll[] }) { const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join(""); - if (actionState.db_error === "invalidPersonValidation") { + if (actionState?.db_error === "invalidPersonValidation") { return ( <>
{actionState.message}
@@ -117,7 +127,7 @@ export default function SignUpForm({ atolls }: { atolls: Atoll[] }) { disabled={isPending} onValueChange={(v) => { console.log({ v }) - setAtoll(atolls.find((atoll) => atoll.id === Number.parseInt(v))); + setAtoll(atolls?.data.find((atoll) => atoll.id === Number.parseInt(v))); }} name="atoll_id" value={atoll?.id?.toString() ?? ""} @@ -128,7 +138,7 @@ export default function SignUpForm({ atolls }: { atolls: Atoll[] }) { Atolls - {atolls.map((atoll) => ( + {atolls?.data.map((atoll) => ( {atoll.name} diff --git a/lib/backend-types.ts b/lib/backend-types.ts index 4f5f878..21fc409 100644 --- a/lib/backend-types.ts +++ b/lib/backend-types.ts @@ -9,7 +9,7 @@ export interface Meta { last_page: number; } -export interface DataResponse { +export interface ApiResponse { meta: Meta; links: Links; data: T[]; diff --git a/middleware.ts b/middleware.ts index f4d2e6d..51174b5 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,15 +6,5 @@ export default withAuth( ); export const config = { - // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - "/((?!api|_next/static|_next/image|favicon.ico|auth/|access-denied).*)", - ], + matcher: ["/about/:path*", "/dashboard/:path*"], }; diff --git a/next-auth.d.ts b/next-auth.d.ts new file mode 100644 index 0000000..9b64aed --- /dev/null +++ b/next-auth.d.ts @@ -0,0 +1,19 @@ +import NextAuth, { DefaultSession } from "next-auth"; +import { Session } from "next-auth"; +import type { User } from "./userTypes"; +declare module "next-auth" { + /** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ + + interface Session { + apiToken?: string; + name?: string | null; + email?: string | null; + image?: string | null; + user?: User & { + expiry?: string; + }; + expires: ISODateString; + } +} diff --git a/providers/AuthProvider.tsx b/providers/AuthProvider.tsx new file mode 100644 index 0000000..2a2f496 --- /dev/null +++ b/providers/AuthProvider.tsx @@ -0,0 +1,13 @@ +"use client" + +import type { Session } from "next-auth" +import { SessionProvider } from "next-auth/react" + +type Props = { + children: React.ReactNode + session?: Session +} + +export const AuthProvider = ({ children, session }: Props) => { + return {children} +} \ No newline at end of file diff --git a/components/query-provider.tsx b/providers/query-provider.tsx similarity index 100% rename from components/query-provider.tsx rename to providers/query-provider.tsx diff --git a/components/theme-provider.tsx b/providers/theme-provider.tsx similarity index 100% rename from components/theme-provider.tsx rename to providers/theme-provider.tsx diff --git a/queries/authentication.ts b/queries/authentication.ts index ac18613..2f012ad 100644 --- a/queries/authentication.ts +++ b/queries/authentication.ts @@ -46,3 +46,21 @@ export async function logout({ token }: { token: string }) { // Since the API endpoint returns 204 No Content on success, we don't need to parse JSON return null; // Return null to indicate a successful logout with no content } + +export async function checkIdOrPhone({ + id_card, + phone_number, +}: { id_card?: string; phone_number?: string }) { + console.log("id_card and phone_number", { id_card, phone_number }); + const response = await fetch( + `${process.env.SARLINK_API_BASE_URL}/api/auth/users/filter/?id_card=${id_card}&mobile=${phone_number}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + const data = await response.json(); + return data; +} diff --git a/queries/islands.ts b/queries/islands.ts index 04ca0b9..e4dda2e 100644 --- a/queries/islands.ts +++ b/queries/islands.ts @@ -1,12 +1,47 @@ "use server"; +import { AxiosClient } from "@/utils/AxiosClient"; + export async function getIslands() { - const res = await fetch(`${process.env.SARLINK_API_BASE_URL}/islands/`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - const data = await res.json(); + const response = await AxiosClient.get("/islands/"); + const data = response.data; return data; } + +export async function getAtolls() { + const response = await fetch( + `${process.env.SARLINK_API_BASE_URL}/api/auth/atolls/`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + const data = response.json(); + return data; +} + +export async function getAllItems({ + limit = 10, + offset = 0, + ...otherParams +}: { + limit?: number; + offset?: number; +} & Record) { + const params = new URLSearchParams(); + // Add default params + params.append("limit", limit.toString()); + params.append("offset", offset.toString()); + + // Add any additional params dynamically + Object.entries(otherParams).map(([key, value]) => { + if (value !== undefined) { + params.append(key, String(value || "")); + } + }); + + const response = await AxiosClient.get(`/inventory/?${params.toString()}`); + return response.data; +} diff --git a/utils/AxiosClient.ts b/utils/AxiosClient.ts new file mode 100644 index 0000000..fe8e196 --- /dev/null +++ b/utils/AxiosClient.ts @@ -0,0 +1,55 @@ +import axios, { type AxiosError } from "axios"; +import type { Session } from "next-auth"; +import { getSession } from "next-auth/react"; +import { redirect } from "next/navigation"; + +axios.defaults.xsrfCookieName = "csrftoken"; +axios.defaults.xsrfHeaderName = "X-CSRFToken"; + +const ApiClient = () => { + const instance = axios.create({ + baseURL: process.env.SARLINK_API_BASE_URL, + headers: { + Accept: "application/json", + }, + }); + + let lastSession: Session | null = null; + + instance.interceptors.request.use( + async (request) => { + if (lastSession == null || Date.now() > Date.parse(lastSession.expires)) { + const session = await getSession(); + lastSession = session; + } + + if (lastSession) { + request.headers.Authorization = `Token ${lastSession.apiToken}`; + } else { + request.headers.Authorization = undefined; + return redirect("/auth/signin"); + } + + return request; + }, + (error) => { + console.error("API Error: ", error); + throw error; + }, + ); + instance.interceptors.response.use( + async (response) => { + return response; + }, + async (error: AxiosError) => { + if (error?.response?.status === 401) { + return redirect("/auth/signin"); + } + return Promise.reject(error); + }, + ); + + return instance; +}; + +export const AxiosClient = ApiClient();