mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-04-19 14:46:52 +00:00
refactor: enhance authentication and signup flow with new providers, update middleware matcher, and improve type safety for API responses
This commit is contained in:
parent
32bb01b656
commit
99c5fef748
@ -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) => {
|
||||
|
@ -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.
|
||||
</p>
|
||||
</div>
|
||||
<SignUpForm atolls={atolls.data} />
|
||||
<SignUpForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${barlow.variable} antialiased font-sans bg-gray-100 dark:bg-black`}>
|
||||
<Provider>
|
||||
<NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
|
||||
<Toaster richColors />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
<AuthProvider session={session || undefined}>
|
||||
<Provider>
|
||||
<NextTopLoader color="#f49d1b" showSpinner={false} zIndex={9999} />
|
||||
<Toaster richColors />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<QueryProvider>
|
||||
{children}
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
@ -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<ApiResponse<Atoll>>({
|
||||
queryKey: ["ATOLLS"],
|
||||
queryFn: () =>
|
||||
getAtolls(),
|
||||
placeholderData: keepPreviousData,
|
||||
staleTime: 1,
|
||||
});
|
||||
|
||||
const [atoll, setAtoll] = React.useState<Atoll>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="h-24 w-72 text-center text-green-500 p-4 flex my-4 flex-col items-center justify-center border dark:title-bg bg-white dark:bg-black rounded-lg">{actionState.message}</div>
|
||||
@ -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[] }) {
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Atolls</SelectLabel>
|
||||
{atolls.map((atoll) => (
|
||||
{atolls?.data.map((atoll) => (
|
||||
<SelectItem key={atoll.id} value={atoll.id.toString()}>
|
||||
{atoll.name}
|
||||
</SelectItem>
|
||||
|
@ -9,7 +9,7 @@ export interface Meta {
|
||||
last_page: number;
|
||||
}
|
||||
|
||||
export interface DataResponse<T> {
|
||||
export interface ApiResponse<T> {
|
||||
meta: Meta;
|
||||
links: Links;
|
||||
data: T[];
|
||||
|
@ -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*"],
|
||||
};
|
||||
|
19
next-auth.d.ts
vendored
Normal file
19
next-auth.d.ts
vendored
Normal file
@ -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;
|
||||
}
|
||||
}
|
13
providers/AuthProvider.tsx
Normal file
13
providers/AuthProvider.tsx
Normal file
@ -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 <SessionProvider session={session}>{children}</SessionProvider>
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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<string, unknown>) {
|
||||
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;
|
||||
}
|
||||
|
55
utils/AxiosClient.ts
Normal file
55
utils/AxiosClient.ts
Normal file
@ -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();
|
Loading…
x
Reference in New Issue
Block a user