mirror of
				https://github.com/i701/sarlink-portal.git
				synced 2025-10-31 16:07:00 +00:00 
			
		
		
		
	refactor: update authentication flow to use NextAuth, replace better-auth with axios for API calls, and clean up unused code
This commit is contained in:
		| @@ -1,10 +1,7 @@ | |||||||
| "use server"; | "use server"; | ||||||
|  |  | ||||||
| import { authClient } from "@/lib/auth-client"; |  | ||||||
| import prisma from "@/lib/db"; |  | ||||||
| import { VerifyUserDetails } from "@/lib/person"; | import { VerifyUserDetails } from "@/lib/person"; | ||||||
| import { signUpFormSchema } from "@/lib/schemas"; | import { signUpFormSchema } from "@/lib/schemas"; | ||||||
| import { phoneNumber } from "better-auth/plugins"; |  | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||||
| import { z } from "zod"; | import { z } from "zod"; | ||||||
| @@ -34,7 +31,7 @@ export async function signin(previousState: ActionState, formData: FormData) { | |||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 	const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`; | 	const FORMATTED_MOBILE_NUMBER: string = `${phoneNumber.split("-").join("")}`; | ||||||
| 	console.log(FORMATTED_MOBILE_NUMBER); | 	console.log({ FORMATTED_MOBILE_NUMBER }); | ||||||
| 	const userExistsResponse = await fetch( | 	const userExistsResponse = await fetch( | ||||||
| 		`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`, | 		`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`, | ||||||
| 		{ | 		{ | ||||||
| @@ -48,7 +45,7 @@ export async function signin(previousState: ActionState, formData: FormData) { | |||||||
| 		}, | 		}, | ||||||
| 	); | 	); | ||||||
| 	const userExists = await userExistsResponse.json(); | 	const userExists = await userExistsResponse.json(); | ||||||
| 	console.log(userExists.non_field_errors); | 	console.log("user exists", userExists); | ||||||
| 	if (userExists?.non_field_errors) { | 	if (userExists?.non_field_errors) { | ||||||
| 		return redirect(`/signup?phone_number=${phoneNumber}`); | 		return redirect(`/signup?phone_number=${phoneNumber}`); | ||||||
| 	} | 	} | ||||||
| @@ -75,7 +72,6 @@ export async function signup(_actionState: ActionState, formData: FormData) { | |||||||
| 	const data = Object.fromEntries(formData.entries()); | 	const data = Object.fromEntries(formData.entries()); | ||||||
| 	const parsedData = signUpFormSchema.safeParse(data); | 	const parsedData = signUpFormSchema.safeParse(data); | ||||||
| 	// get phone number from /signup?phone_number=999-1231 | 	// get phone number from /signup?phone_number=999-1231 | ||||||
| 	const headersList = await headers(); |  | ||||||
|  |  | ||||||
| 	console.log("DATA ON SERVER SIDE", data); | 	console.log("DATA ON SERVER SIDE", data); | ||||||
|  |  | ||||||
| @@ -87,83 +83,82 @@ export async function signup(_actionState: ActionState, formData: FormData) { | |||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	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, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	if (idCardExists) { | 	// if (idCardExists) { | ||||||
| 		return { | 	// 	return { | ||||||
| 			message: "ID card already exists.", | 	// 		message: "ID card already exists.", | ||||||
| 			payload: formData, | 	// 		payload: formData, | ||||||
| 			db_error: "id_card", | 	// 		db_error: "id_card", | ||||||
| 		}; | 	// 	}; | ||||||
| 	} | 	// } | ||||||
|  |  | ||||||
| 	const phoneNumberExists = await prisma.user.findFirst({ | 	// const phoneNumberExists = await prisma.user.findFirst({ | ||||||
| 		where: { | 	// 	where: { | ||||||
| 			phoneNumber: parsedData.data.phone_number, | 	// 		phoneNumber: parsedData.data.phone_number, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
|  |  | ||||||
| 	if (phoneNumberExists) { | 	// if (phoneNumberExists) { | ||||||
| 		return { | 	// 	return { | ||||||
| 			message: "Phone number already exists.", | 	// 		message: "Phone number already exists.", | ||||||
| 			payload: formData, | 	// 		payload: formData, | ||||||
| 			db_error: "phone_number", | 	// 		db_error: "phone_number", | ||||||
| 		}; | 	// 	}; | ||||||
| 	} | 	// } | ||||||
|  |  | ||||||
| 	const newUser = await prisma.user.create({ | 	// const newUser = await prisma.user.create({ | ||||||
| 		data: { | 	// 	data: { | ||||||
| 			name: parsedData.data.name, | 	// 		name: parsedData.data.name, | ||||||
| 			islandId: parsedData.data.island_id, | 	// 		islandId: parsedData.data.island_id, | ||||||
| 			atollId: parsedData.data.atoll_id, | 	// 		atollId: parsedData.data.atoll_id, | ||||||
| 			address: parsedData.data.address, | 	// 		address: parsedData.data.address, | ||||||
| 			id_card: parsedData.data.id_card, | 	// 		id_card: parsedData.data.id_card, | ||||||
| 			dob: new Date(parsedData.data.dob), | 	// 		dob: new Date(parsedData.data.dob), | ||||||
| 			role: "USER", | 	// 		role: "USER", | ||||||
| 			accNo: parsedData.data.accNo, | 	// 		accNo: parsedData.data.accNo, | ||||||
| 			phoneNumber: parsedData.data.phone_number, | 	// 		phoneNumber: parsedData.data.phone_number, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
| 	const isValidPerson = await VerifyUserDetails({ user: newUser }); | 	// const isValidPerson = await VerifyUserDetails({ user: newUser }); | ||||||
|  |  | ||||||
| 	if (!isValidPerson) { | 	// 	if (!isValidPerson) { | ||||||
| 		await SendUserRejectionDetailSMS({ | 	// 		await SendUserRejectionDetailSMS({ | ||||||
| 			details: ` | 	// 			details: ` | ||||||
| 			A new user has requested for verification. \n | 	// 			A new user has requested for verification. \n | ||||||
| USER DETAILS: | 	// USER DETAILS: | ||||||
| Name: ${parsedData.data.name} | 	// Name: ${parsedData.data.name} | ||||||
| Address: ${parsedData.data.address} | 	// Address: ${parsedData.data.address} | ||||||
| ID Card: ${parsedData.data.id_card} | 	// ID Card: ${parsedData.data.id_card} | ||||||
| DOB: ${parsedData.data.dob.toLocaleDateString("en-US", { | 	// DOB: ${parsedData.data.dob.toLocaleDateString("en-US", { | ||||||
| 				month: "short", | 	// 				month: "short", | ||||||
| 				day: "2-digit", | 	// 				day: "2-digit", | ||||||
| 				year: "numeric", | 	// 				year: "numeric", | ||||||
| 			})} | 	// 			})} | ||||||
| ACC No: ${parsedData.data.accNo}\n\nVerify the user with the following link: ${process.env.BETTER_AUTH_URL}/users/${newUser.id}/verify | 	// ACC No: ${parsedData.data.accNo}\n\nVerify the user with the following link: ${process.env.BETTER_AUTH_URL}/users/${newUser.id}/verify | ||||||
| 			`, | 	// 			`, | ||||||
| 			phoneNumber: process.env.ADMIN_PHONENUMBER ?? "", | 	// 			phoneNumber: process.env.ADMIN_PHONENUMBER ?? "", | ||||||
| 		}); | 	// 		}); | ||||||
| 		return { | 	// 		return { | ||||||
| 			message: | 	// 			message: | ||||||
| 				"Your account has been requested for verification. Please wait for a response from admin.", | 	// 				"Your account has been requested for verification. Please wait for a response from admin.", | ||||||
| 			payload: formData, | 	// 			payload: formData, | ||||||
| 			db_error: "invalidPersonValidation", | 	// 			db_error: "invalidPersonValidation", | ||||||
| 		}; | 	// 		}; | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if (isValidPerson) { | 	// if (isValidPerson) { | ||||||
| 		await authClient.phoneNumber.sendOtp({ | 	// 	await authClient.phoneNumber.sendOtp({ | ||||||
| 			phoneNumber: newUser.phoneNumber, | 	// 		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: "User created successfully" }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const sendOtp = async (phoneNumber: string, code: string) => { | export const sendOtp = async (phoneNumber: string, code: string) => { | ||||||
|   | |||||||
| @@ -1,85 +1,79 @@ | |||||||
| "use server"; | "use server"; | ||||||
|  |  | ||||||
| import prisma from "@/lib/db"; |  | ||||||
| import { VerifyUserDetails } from "@/lib/person"; | 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"; | ||||||
|  |  | ||||||
| export async function VerifyUser(userId: string) { | export async function VerifyUser(userId: string) { | ||||||
| 	const user = await prisma.user.findUnique({ | 	// const user = await prisma.user.findUnique({ | ||||||
| 		where: { | 	// 	where: { | ||||||
| 			id: userId, | 	// 		id: userId, | ||||||
| 		}, | 	// 	}, | ||||||
| 		include: { | 	// 	include: { | ||||||
| 			atoll: true, | 	// 		atoll: true, | ||||||
| 			island: true, | 	// 		island: true, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
| 	if (!user) { | 	// if (!user) { | ||||||
| 		throw new Error("User not found"); | 	// 	throw new Error("User not found"); | ||||||
| 	} | 	// } | ||||||
| 	const isValidPerson = await VerifyUserDetails({ user }); | 	// const isValidPerson = await VerifyUserDetails({ user }); | ||||||
|  | 	// if (!isValidPerson) | ||||||
| 	if (!isValidPerson) | 	// 	throw new Error("The user details does not match national data."); | ||||||
| 		throw new Error("The user details does not match national data."); | 	// if (isValidPerson) { | ||||||
|  | 	// 	await prisma.user.update({ | ||||||
| 	if (isValidPerson) { | 	// 		where: { | ||||||
| 		await prisma.user.update({ | 	// 			id: userId, | ||||||
| 			where: { | 	// 		}, | ||||||
| 				id: userId, | 	// 		data: { | ||||||
| 			}, | 	// 			verified: true, | ||||||
| 			data: { | 	// 		}, | ||||||
| 				verified: true, | 	// 	}); | ||||||
| 			}, | 	// const ninjaClient = await CreateClient({ | ||||||
| 		}); | 	// 	group_settings_id: "", | ||||||
|  | 	// 	address1: "", | ||||||
| 		const ninjaClient = await CreateClient({ | 	// 	city: user.atoll?.name || "", | ||||||
| 			group_settings_id: "", | 	// 	state: user.island?.name || "", | ||||||
| 			address1: "", | 	// 	postal_code: "", | ||||||
| 			city: user.atoll?.name || "", | 	// 	country_id: "462", | ||||||
| 			state: user.island?.name || "", | 	// 	address2: user.address || "", | ||||||
| 			postal_code: "", | 	// 	contacts: { | ||||||
| 			country_id: "462", | 	// 		first_name: user.name?.split(" ")[0] || "", | ||||||
| 			address2: user.address || "", | 	// 		last_name: user.name?.split(" ")[1] || "", | ||||||
| 			contacts: { | 	// 		email: user.email || "", | ||||||
| 				first_name: user.name?.split(" ")[0] || "", | 	// 		phone: user.phoneNumber || "", | ||||||
| 				last_name: user.name?.split(" ")[1] || "", | 	// 		send_email: false, | ||||||
| 				email: user.email || "", | 	// 		custom_value1: user.dob?.toISOString().split("T")[0] || "", | ||||||
| 				phone: user.phoneNumber || "", | 	// 		custom_value2: user.id_card || "", | ||||||
| 				send_email: false, | 	// 		custom_value3: "", | ||||||
| 				custom_value1: user.dob?.toISOString().split("T")[0] || "", | 	// 	}, | ||||||
| 				custom_value2: user.id_card || "", | 	// }); | ||||||
| 				custom_value3: "", | 	// } | ||||||
| 			}, | 	// revalidatePath("/users"); | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	revalidatePath("/users"); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function Rejectuser({ | export async function Rejectuser({ | ||||||
| 	userId, | 	userId, | ||||||
| 	reason, | 	reason, | ||||||
| }: { userId: string; reason: string }) { | }: { userId: string; reason: string }) { | ||||||
| 	const user = await prisma.user.findUnique({ | 	// const user = await prisma.user.findUnique({ | ||||||
| 		where: { | 	// 	where: { | ||||||
| 			id: userId, | 	// 		id: userId, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
| 	if (!user) { | 	// if (!user) { | ||||||
| 		throw new Error("User not found"); | 	// 	throw new Error("User not found"); | ||||||
| 	} | 	// } | ||||||
|  |  | ||||||
| 	await SendUserRejectionDetailSMS({ | 	// await SendUserRejectionDetailSMS({ | ||||||
| 		details: reason, | 	// 	details: reason, | ||||||
| 		phoneNumber: user.phoneNumber, | 	// 	phoneNumber: user.phoneNumber, | ||||||
| 	}); | 	// }); | ||||||
| 	await prisma.user.delete({ | 	// await prisma.user.delete({ | ||||||
| 		where: { | 	// 	where: { | ||||||
| 			id: userId, | 	// 		id: userId, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
| 	revalidatePath("/users"); | 	revalidatePath("/users"); | ||||||
| 	redirect("/users"); | 	redirect("/users"); | ||||||
| } | } | ||||||
| @@ -117,13 +111,13 @@ export async function AddDevice({ | |||||||
| 	mac_address, | 	mac_address, | ||||||
| 	user_id, | 	user_id, | ||||||
| }: { name: string; mac_address: string; user_id: string }) { | }: { name: string; mac_address: string; user_id: string }) { | ||||||
| 	const newDevice = await prisma.device.create({ | 	// const newDevice = await prisma.device.create({ | ||||||
| 		data: { | 	// 	data: { | ||||||
| 			name: name, | 	// 		name: name, | ||||||
| 			mac: mac_address, | 	// 		mac: mac_address, | ||||||
| 			userId: user_id, | 	// 		userId: user_id, | ||||||
| 		}, | 	// 	}, | ||||||
| 	}); | 	// }); | ||||||
| 	revalidatePath("/devices"); | 	revalidatePath("/devices"); | ||||||
| 	return newDevice; | 	// return newDevice; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| import LoginForm from "@/components/auth/login-form"; | import LoginForm from "@/components/auth/login-form"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import Image from "next/image"; | import Image from "next/image"; | ||||||
| import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
|  |  | ||||||
| export default async function LoginPage() { | export default async function LoginPage() { | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<div className="dark:bg-black w-full h-screen flex items-center justify-center font-sans"> | 		<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 "> | 			<div className="flex flex-col items-center justify-center w-full h-full "> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import DevicesToPay from "@/components/devices-to-pay"; | import DevicesToPay from "@/components/devices-to-pay"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import prisma from "@/lib/db"; | import prisma from "@/lib/db"; | ||||||
| import { cn } from "@/lib/utils"; | import { cn } from "@/lib/utils"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| @@ -10,13 +10,13 @@ export default async function PaymentPage({ | |||||||
| 	params: Promise<{ paymentId: string }>; | 	params: Promise<{ paymentId: string }>; | ||||||
| }) { | }) { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
|     headers: await headers() | 		headers: await headers(), | ||||||
|   }) | 	}); | ||||||
| 	const user = await prisma.user.findUnique({ | 	const user = await prisma.user.findUnique({ | ||||||
| 		where: { | 		where: { | ||||||
|       id: session?.session.userId | 			id: session?.session.userId, | ||||||
|     } | 		}, | ||||||
|   }) | 	}); | ||||||
| 	const paymentId = (await params).paymentId; | 	const paymentId = (await params).paymentId; | ||||||
| 	const payment = await prisma.payment.findUnique({ | 	const payment = await prisma.payment.findUnique({ | ||||||
| 		where: { | 		where: { | ||||||
| @@ -29,10 +29,15 @@ export default async function PaymentPage({ | |||||||
| 	return ( | 	return ( | ||||||
| 		<div> | 		<div> | ||||||
| 			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> | 			<div className="flex justify-between items-center border-[1px] rounded-md border-dashed font-bold title-bg py-4 px-2 mb-4"> | ||||||
|         <h3 className="text-sarLinkOrange text-2xl"> | 				<h3 className="text-sarLinkOrange text-2xl">Payment</h3> | ||||||
|           Payment | 				<span | ||||||
|         </h3> | 					className={cn( | ||||||
|         <span className={cn("text-sm border px-4 py-2 rounded-md uppercase font-semibold", payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-700")}> | 						"text-sm border px-4 py-2 rounded-md uppercase font-semibold", | ||||||
|  | 						payment?.paid | ||||||
|  | 							? "text-green-500 bg-green-500/20" | ||||||
|  | 							: "text-yellow-500 bg-yellow-700", | ||||||
|  | 					)} | ||||||
|  | 				> | ||||||
| 					{payment?.paid ? "Paid" : "Pending"} | 					{payment?.paid ? "Paid" : "Pending"} | ||||||
| 				</span> | 				</span> | ||||||
| 			</div> | 			</div> | ||||||
| @@ -41,10 +46,7 @@ export default async function PaymentPage({ | |||||||
| 				id="user-filters" | 				id="user-filters" | ||||||
| 				className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" | 				className="pb-4 gap-4 flex sm:flex-row flex-col items-start justify-start" | ||||||
| 			> | 			> | ||||||
|         <DevicesToPay | 				<DevicesToPay user={user || undefined} payment={payment || undefined} /> | ||||||
|           user={user || undefined} |  | ||||||
|           payment={payment || undefined} |  | ||||||
|         /> |  | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	); | 	); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import { toNextJsHandler } from "better-auth/next-js"; | import { toNextJsHandler } from "better-auth/next-js"; | ||||||
|  |  | ||||||
| export const { GET, POST } = toNextJsHandler(auth.handler); | export const { GET, POST } = toNextJsHandler(auth.handler); | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								app/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								app/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import { logout } from "@/queries/authentication"; | ||||||
|  | import type { NextAuthOptions } from "next-auth"; | ||||||
|  | import type { JWT } from "next-auth/jwt"; | ||||||
|  | import CredentialsProvider from "next-auth/providers/credentials"; | ||||||
|  |  | ||||||
|  | export const authOptions: NextAuthOptions = { | ||||||
|  | 	pages: { | ||||||
|  | 		signIn: "/auth/signin", | ||||||
|  | 	}, | ||||||
|  | 	session: { | ||||||
|  | 		strategy: "jwt", | ||||||
|  | 		maxAge: 30 * 60, // 30 mins | ||||||
|  | 	}, | ||||||
|  | 	events: { | ||||||
|  | 		signOut({ token }) { | ||||||
|  | 			const apitoken = token.apiToken; | ||||||
|  | 			console.log("apitoken", apitoken); | ||||||
|  | 			logout({ token: apitoken as string }); | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	providers: [ | ||||||
|  | 		CredentialsProvider({ | ||||||
|  | 			name: "Credentials", | ||||||
|  | 			credentials: { | ||||||
|  | 				email: { label: "Email", type: "text", placeholder: "jsmith" }, | ||||||
|  | 				password: { label: "Password", type: "password" }, | ||||||
|  | 			}, | ||||||
|  | 			async authorize(credentials) { | ||||||
|  | 				const { email, password } = credentials as { | ||||||
|  | 					email: string; | ||||||
|  | 					password: string; | ||||||
|  | 				}; | ||||||
|  | 				console.log("email and password", email, password); | ||||||
|  | 				const res = await fetch( | ||||||
|  | 					`${process.env.NEXT_PUBLIC_API_URL}/auth/login/`, | ||||||
|  | 					{ | ||||||
|  | 						method: "POST", | ||||||
|  | 						headers: { | ||||||
|  | 							"Content-Type": "application/json", | ||||||
|  | 						}, | ||||||
|  | 						body: JSON.stringify({ | ||||||
|  | 							username: email, | ||||||
|  | 							password: password, | ||||||
|  | 						}), | ||||||
|  | 					}, | ||||||
|  | 				); | ||||||
|  | 				console.log("status", res.status); | ||||||
|  |  | ||||||
|  | 				const data = await res.json(); | ||||||
|  | 				console.log({ data }); | ||||||
|  | 				switch (res.status) { | ||||||
|  | 					case 200: | ||||||
|  | 						return { ...data.user, apiToken: data.token, expiry: data.expiry }; | ||||||
|  | 					case 400: | ||||||
|  | 						throw new Error( | ||||||
|  | 							JSON.stringify({ message: data.message, status: res.status }), | ||||||
|  | 						); | ||||||
|  | 					case 429: | ||||||
|  | 						throw new Error( | ||||||
|  | 							JSON.stringify({ message: data.message, status: res.status }), | ||||||
|  | 						); | ||||||
|  | 					case 403: | ||||||
|  | 						throw new Error( | ||||||
|  | 							JSON.stringify({ message: data.error, status: res.status }), | ||||||
|  | 						); | ||||||
|  | 					default: | ||||||
|  | 						throw new Error( | ||||||
|  | 							JSON.stringify({ | ||||||
|  | 								message: "FATAL: Unexprted Error occured!", | ||||||
|  | 								status: res.status, | ||||||
|  | 							}), | ||||||
|  | 						); | ||||||
|  | 				} | ||||||
|  | 			}, | ||||||
|  | 		}), | ||||||
|  | 	], | ||||||
|  | 	callbacks: { | ||||||
|  | 		redirect: async ({ url, baseUrl }) => { | ||||||
|  | 			// Allows relative callback URLs | ||||||
|  | 			if (url.startsWith("/")) return `${baseUrl}${url}`; | ||||||
|  | 			return baseUrl; | ||||||
|  | 		}, | ||||||
|  | 		session: async ({ session, token }) => { | ||||||
|  | 			const sanitizedToken = Object.keys(token).reduce((p, c) => { | ||||||
|  | 				// strip unnecessary properties | ||||||
|  | 				if (c !== "iat" && c !== "exp" && c !== "jti" && c !== "apiToken") { | ||||||
|  | 					Object.assign(p, { [c]: token[c] }); | ||||||
|  | 				} | ||||||
|  | 				return p; | ||||||
|  | 			}, {}); | ||||||
|  | 			// session.expires = token.expiry | ||||||
|  | 			return { | ||||||
|  | 				...session, | ||||||
|  | 				user: sanitizedToken, | ||||||
|  | 				apiToken: token.apiToken, | ||||||
|  | 				// expires: token.expiry, | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		jwt: ({ token, user }) => { | ||||||
|  | 			if (typeof user !== "undefined") { | ||||||
|  | 				// user has just signed in so the user object is populated | ||||||
|  | 				return user as unknown as JWT; | ||||||
|  | 			} | ||||||
|  | 			return token; | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	secret: process.env.NEXTAUTH_SECRET, | ||||||
|  | }; | ||||||
| @@ -8,7 +8,7 @@ import { | |||||||
| 	TableHeader, | 	TableHeader, | ||||||
| 	TableRow, | 	TableRow, | ||||||
| } from "@/components/ui/table"; | } from "@/components/ui/table"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import prisma from "@/lib/db"; | import prisma from "@/lib/db"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
| @@ -28,9 +28,9 @@ export async function AdminDevicesTable({ | |||||||
| 	parentalControl?: boolean; | 	parentalControl?: boolean; | ||||||
| }) { | }) { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
|     headers: await headers() | 		headers: await headers(), | ||||||
|   }) | 	}); | ||||||
|   const isAdmin = session?.user.role === "ADMIN" | 	const isAdmin = session?.user.role === "ADMIN"; | ||||||
| 	const query = (await searchParams)?.query || ""; | 	const query = (await searchParams)?.query || ""; | ||||||
| 	const page = (await searchParams)?.page; | 	const page = (await searchParams)?.page; | ||||||
| 	const sortBy = (await searchParams)?.sortBy || "asc"; | 	const sortBy = (await searchParams)?.sortBy || "asc"; | ||||||
| @@ -119,7 +119,6 @@ export async function AdminDevicesTable({ | |||||||
| 													{device.name} | 													{device.name} | ||||||
| 												</Link> | 												</Link> | ||||||
| 												{device.isActive && ( | 												{device.isActive && ( | ||||||
|  |  | ||||||
| 													<span className="text-muted-foreground"> | 													<span className="text-muted-foreground"> | ||||||
| 														Active until{" "} | 														Active until{" "} | ||||||
| 														{new Date().toLocaleDateString("en-US", { | 														{new Date().toLocaleDateString("en-US", { | ||||||
| @@ -138,10 +137,11 @@ export async function AdminDevicesTable({ | |||||||
| 														</p> | 														</p> | ||||||
| 													</div> | 													</div> | ||||||
| 												)} | 												)} | ||||||
|  |  | ||||||
| 											</div> | 											</div> | ||||||
| 										</TableCell> | 										</TableCell> | ||||||
|                     <TableCell className="font-medium">{device.User?.name}</TableCell> | 										<TableCell className="font-medium"> | ||||||
|  | 											{device.User?.name} | ||||||
|  | 										</TableCell> | ||||||
|  |  | ||||||
| 										<TableCell className="font-medium">{device.mac}</TableCell> | 										<TableCell className="font-medium">{device.mac}</TableCell> | ||||||
| 										<TableCell> | 										<TableCell> | ||||||
| @@ -161,7 +161,11 @@ export async function AdminDevicesTable({ | |||||||
| 											})} | 											})} | ||||||
| 										</TableCell> | 										</TableCell> | ||||||
| 										<TableCell> | 										<TableCell> | ||||||
|                       <BlockDeviceDialog admin={isAdmin} type={device.blocked ? "unblock" : "block"} device={device} /> | 											<BlockDeviceDialog | ||||||
|  | 												admin={isAdmin} | ||||||
|  | 												type={device.blocked ? "unblock" : "block"} | ||||||
|  | 												device={device} | ||||||
|  | 											/> | ||||||
| 										</TableCell> | 										</TableCell> | ||||||
| 									</TableRow> | 									</TableRow> | ||||||
| 								))} | 								))} | ||||||
| @@ -186,7 +190,11 @@ export async function AdminDevicesTable({ | |||||||
| 					</div> | 					</div> | ||||||
| 					<div className="sm:hidden my-4"> | 					<div className="sm:hidden my-4"> | ||||||
| 						{devices.map((device) => ( | 						{devices.map((device) => ( | ||||||
|               <DeviceCard parentalControl={parentalControl} key={device.id} device={device} /> | 							<DeviceCard | ||||||
|  | 								parentalControl={parentalControl} | ||||||
|  | 								key={device.id} | ||||||
|  | 								device={device} | ||||||
|  | 							/> | ||||||
| 						))} | 						))} | ||||||
| 					</div> | 					</div> | ||||||
| 				</> | 				</> | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import { | |||||||
| 	SidebarProvider, | 	SidebarProvider, | ||||||
| 	SidebarTrigger, | 	SidebarTrigger, | ||||||
| } from "@/components/ui/sidebar"; | } from "@/components/ui/sidebar"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import prisma from "@/lib/db"; | import prisma from "@/lib/db"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import { AccountPopover } from "./account-popver"; | import { AccountPopover } from "./account-popver"; | ||||||
| @@ -19,7 +19,7 @@ export async function ApplicationLayout({ | |||||||
| 	children, | 	children, | ||||||
| }: { children: React.ReactNode }) { | }: { children: React.ReactNode }) { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
| 		headers: await headers() | 		headers: await headers(), | ||||||
| 	}); | 	}); | ||||||
| 	const billFormula = await prisma.billFormula.findFirst(); | 	const billFormula = await prisma.billFormula.findFirst(); | ||||||
| 	const user = await prisma.user.findFirst({ | 	const user = await prisma.user.findFirst({ | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ export default function LoginForm() { | |||||||
| 					<PhoneInput | 					<PhoneInput | ||||||
| 						id="phone-number" | 						id="phone-number" | ||||||
| 						name="phoneNumber" | 						name="phoneNumber" | ||||||
|  | 						className="b0rder" | ||||||
| 						maxLength={8} | 						maxLength={8} | ||||||
| 						disabled={isPending} | 						disabled={isPending} | ||||||
| 						placeholder="Enter phone number" | 						placeholder="Enter phone number" | ||||||
| @@ -32,11 +33,7 @@ export default function LoginForm() { | |||||||
| 					{state.status === "error" && ( | 					{state.status === "error" && ( | ||||||
| 						<p className="text-red-500 text-sm">{state.message}</p> | 						<p className="text-red-500 text-sm">{state.message}</p> | ||||||
| 					)} | 					)} | ||||||
| 					<Button | 					<Button className="" disabled={isPending} type="submit"> | ||||||
| 						className="" |  | ||||||
| 						disabled={isPending} |  | ||||||
| 						type="submit" |  | ||||||
| 					> |  | ||||||
| 						{isPending ? <Loader2 className="animate-spin" /> : "Request OTP"} | 						{isPending ? <Loader2 className="animate-spin" /> : "Request OTP"} | ||||||
| 					</Button> | 					</Button> | ||||||
| 				</div> | 				</div> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { | |||||||
| 	TableHeader, | 	TableHeader, | ||||||
| 	TableRow, | 	TableRow, | ||||||
| } from "@/components/ui/table"; | } from "@/components/ui/table"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import prisma from "@/lib/db"; | import prisma from "@/lib/db"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import ClickableRow from "./clickable-row"; | import ClickableRow from "./clickable-row"; | ||||||
| @@ -27,9 +27,9 @@ export async function DevicesTable({ | |||||||
| 	parentalControl?: boolean; | 	parentalControl?: boolean; | ||||||
| }) { | }) { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
| 		headers: await headers() | 		headers: await headers(), | ||||||
| 	}) | 	}); | ||||||
| 	const isAdmin = session?.user.role === "ADMIN" | 	const isAdmin = session?.user.role === "ADMIN"; | ||||||
| 	const query = (await searchParams)?.query || ""; | 	const query = (await searchParams)?.query || ""; | ||||||
| 	const page = (await searchParams)?.page; | 	const page = (await searchParams)?.page; | ||||||
| 	const sortBy = (await searchParams)?.sortBy || "asc"; | 	const sortBy = (await searchParams)?.sortBy || "asc"; | ||||||
| @@ -53,12 +53,16 @@ export async function DevicesTable({ | |||||||
| 			NOT: { | 			NOT: { | ||||||
| 				payments: { | 				payments: { | ||||||
| 					some: { | 					some: { | ||||||
| 						paid: false | 						paid: false, | ||||||
| 					} | 					}, | ||||||
| 				} | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isActive: isAdmin ? undefined : parentalControl, | 			isActive: isAdmin ? undefined : parentalControl, | ||||||
| 			blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false, | 			blocked: isAdmin | ||||||
|  | 				? undefined | ||||||
|  | 				: parentalControl !== undefined | ||||||
|  | 					? undefined | ||||||
|  | 					: false, | ||||||
| 		}, | 		}, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| @@ -86,8 +90,8 @@ export async function DevicesTable({ | |||||||
| 			NOT: { | 			NOT: { | ||||||
| 				payments: { | 				payments: { | ||||||
| 					some: { | 					some: { | ||||||
| 						paid: false | 						paid: false, | ||||||
| 					} | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isActive: parentalControl, | 			isActive: parentalControl, | ||||||
| @@ -158,7 +162,12 @@ export async function DevicesTable({ | |||||||
| 									// 		)} | 									// 		)} | ||||||
| 									// 	</TableCell> | 									// 	</TableCell> | ||||||
| 									// </TableRow> | 									// </TableRow> | ||||||
| 									<ClickableRow admin={isAdmin} key={device.id} device={device} parentalControl={parentalControl} /> | 									<ClickableRow | ||||||
|  | 										admin={isAdmin} | ||||||
|  | 										key={device.id} | ||||||
|  | 										device={device} | ||||||
|  | 										parentalControl={parentalControl} | ||||||
|  | 									/> | ||||||
| 								))} | 								))} | ||||||
| 							</TableBody> | 							</TableBody> | ||||||
| 							<TableFooter> | 							<TableFooter> | ||||||
| @@ -181,7 +190,11 @@ export async function DevicesTable({ | |||||||
| 					</div> | 					</div> | ||||||
| 					<div className="sm:hidden my-4"> | 					<div className="sm:hidden my-4"> | ||||||
| 						{devices.map((device) => ( | 						{devices.map((device) => ( | ||||||
| 							<DeviceCard parentalControl={parentalControl} key={device.id} device={device} /> | 							<DeviceCard | ||||||
|  | 								parentalControl={parentalControl} | ||||||
|  | 								key={device.id} | ||||||
|  | 								device={device} | ||||||
|  | 							/> | ||||||
| 						))} | 						))} | ||||||
| 					</div> | 					</div> | ||||||
| 				</> | 				</> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { | |||||||
| import prisma from "@/lib/db"; | import prisma from "@/lib/db"; | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
|  |  | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import { cn } from "@/lib/utils"; | import { cn } from "@/lib/utils"; | ||||||
| import type { Prisma } from "@prisma/client"; | import type { Prisma } from "@prisma/client"; | ||||||
| import { Calendar } from "lucide-react"; | import { Calendar } from "lucide-react"; | ||||||
| @@ -25,7 +25,7 @@ type PaymentWithDevices = Prisma.PaymentGetPayload<{ | |||||||
| 	include: { | 	include: { | ||||||
| 		devices: true; | 		devices: true; | ||||||
| 	}; | 	}; | ||||||
| }> | }>; | ||||||
|  |  | ||||||
| export async function PaymentsTable({ | export async function PaymentsTable({ | ||||||
| 	searchParams, | 	searchParams, | ||||||
| @@ -37,8 +37,8 @@ export async function PaymentsTable({ | |||||||
| 	}>; | 	}>; | ||||||
| }) { | }) { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
|     headers: await headers() | 		headers: await headers(), | ||||||
|   }) | 	}); | ||||||
| 	const query = (await searchParams)?.query || ""; | 	const query = (await searchParams)?.query || ""; | ||||||
| 	const page = (await searchParams)?.page; | 	const page = (await searchParams)?.page; | ||||||
| 	const totalPayments = await prisma.payment.count({ | 	const totalPayments = await prisma.payment.count({ | ||||||
| @@ -80,7 +80,7 @@ export async function PaymentsTable({ | |||||||
| 			], | 			], | ||||||
| 		}, | 		}, | ||||||
| 		include: { | 		include: { | ||||||
|       devices: true | 			devices: true, | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		skip: offset, | 		skip: offset, | ||||||
| @@ -113,25 +113,45 @@ export async function PaymentsTable({ | |||||||
| 								{payments.map((payment) => ( | 								{payments.map((payment) => ( | ||||||
| 									<TableRow key={payment.id}> | 									<TableRow key={payment.id}> | ||||||
| 										<TableCell> | 										<TableCell> | ||||||
|                       <div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}> | 											<div | ||||||
|  | 												className={cn( | ||||||
|  | 													"flex flex-col items-start border rounded p-2", | ||||||
|  | 													payment?.paid | ||||||
|  | 														? "bg-green-500/10 border-dashed border-green=500" | ||||||
|  | 														: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", | ||||||
|  | 												)} | ||||||
|  | 											> | ||||||
| 												<div className="flex items-center gap-2"> | 												<div className="flex items-center gap-2"> | ||||||
| 													<Calendar size={16} opacity={0.5} /> | 													<Calendar size={16} opacity={0.5} /> | ||||||
| 													<span className="text-muted-foreground"> | 													<span className="text-muted-foreground"> | ||||||
|                             {new Date(payment.createdAt).toLocaleDateString("en-US", { | 														{new Date(payment.createdAt).toLocaleDateString( | ||||||
|  | 															"en-US", | ||||||
|  | 															{ | ||||||
| 																month: "short", | 																month: "short", | ||||||
| 																day: "2-digit", | 																day: "2-digit", | ||||||
| 																year: "numeric", | 																year: "numeric", | ||||||
|                             })} | 															}, | ||||||
|  | 														)} | ||||||
| 													</span> | 													</span> | ||||||
| 												</div> | 												</div> | ||||||
|  |  | ||||||
| 												<div className="flex items-center gap-2 mt-2"> | 												<div className="flex items-center gap-2 mt-2"> | ||||||
|                           <Link className="font-medium hover:underline" href={`/payments/${payment.id}`}> | 													<Link | ||||||
|  | 														className="font-medium hover:underline" | ||||||
|  | 														href={`/payments/${payment.id}`} | ||||||
|  | 													> | ||||||
| 														<Button size={"sm"} variant="outline"> | 														<Button size={"sm"} variant="outline"> | ||||||
| 															View Details | 															View Details | ||||||
| 														</Button> | 														</Button> | ||||||
| 													</Link> | 													</Link> | ||||||
|                           <Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}> | 													<Badge | ||||||
|  | 														className={cn( | ||||||
|  | 															payment?.paid | ||||||
|  | 																? "text-green-500 bg-green-500/20" | ||||||
|  | 																: "text-yellow-500 bg-yellow-500/20", | ||||||
|  | 														)} | ||||||
|  | 														variant={payment.paid ? "outline" : "secondary"} | ||||||
|  | 													> | ||||||
| 														{payment.paid ? "Paid" : "Unpaid"} | 														{payment.paid ? "Paid" : "Unpaid"} | ||||||
| 													</Badge> | 													</Badge> | ||||||
| 												</div> | 												</div> | ||||||
| @@ -139,7 +159,10 @@ export async function PaymentsTable({ | |||||||
| 													<h3 className="text-sm font-medium">Devices</h3> | 													<h3 className="text-sm font-medium">Devices</h3> | ||||||
| 													<ol className="list-disc list-inside text-sm"> | 													<ol className="list-disc list-inside text-sm"> | ||||||
| 														{payment.devices.map((device) => ( | 														{payment.devices.map((device) => ( | ||||||
|                               <li key={device.id} className="text-sm text-muted-foreground"> | 															<li | ||||||
|  | 																key={device.id} | ||||||
|  | 																className="text-sm text-muted-foreground" | ||||||
|  | 															> | ||||||
| 																{device.name} | 																{device.name} | ||||||
| 															</li> | 															</li> | ||||||
| 														))} | 														))} | ||||||
| @@ -188,10 +211,16 @@ export async function PaymentsTable({ | |||||||
| 	); | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) { | function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) { | ||||||
| 	return ( | 	return ( | ||||||
|     <div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}> | 		<div | ||||||
|  | 			className={cn( | ||||||
|  | 				"flex flex-col items-start border rounded p-2", | ||||||
|  | 				payment?.paid | ||||||
|  | 					? "bg-green-500/10 border-dashed border-green=500" | ||||||
|  | 					: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50", | ||||||
|  | 			)} | ||||||
|  | 		> | ||||||
| 			<div className="flex items-center gap-2"> | 			<div className="flex items-center gap-2"> | ||||||
| 				<Calendar size={16} opacity={0.5} /> | 				<Calendar size={16} opacity={0.5} /> | ||||||
| 				<span className="text-muted-foreground text-sm"> | 				<span className="text-muted-foreground text-sm"> | ||||||
| @@ -204,12 +233,22 @@ function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) { | |||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
| 			<div className="flex items-center gap-2 mt-2"> | 			<div className="flex items-center gap-2 mt-2"> | ||||||
|         <Link className="font-medium hover:underline" href={`/payments/${payment.id}`}> | 				<Link | ||||||
|  | 					className="font-medium hover:underline" | ||||||
|  | 					href={`/payments/${payment.id}`} | ||||||
|  | 				> | ||||||
| 					<Button size={"sm"} variant="outline"> | 					<Button size={"sm"} variant="outline"> | ||||||
| 						View Details | 						View Details | ||||||
| 					</Button> | 					</Button> | ||||||
| 				</Link> | 				</Link> | ||||||
|         <Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}> | 				<Badge | ||||||
|  | 					className={cn( | ||||||
|  | 						payment?.paid | ||||||
|  | 							? "text-green-500 bg-green-500/20" | ||||||
|  | 							: "text-yellow-500 bg-yellow-500/20", | ||||||
|  | 					)} | ||||||
|  | 					variant={payment.paid ? "outline" : "secondary"} | ||||||
|  | 				> | ||||||
| 					{payment.paid ? "Paid" : "Unpaid"} | 					{payment.paid ? "Paid" : "Unpaid"} | ||||||
| 				</Badge> | 				</Badge> | ||||||
| 			</div> | 			</div> | ||||||
| @@ -236,5 +275,5 @@ function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) { | |||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|   ) | 	); | ||||||
| } | } | ||||||
| @@ -61,11 +61,7 @@ const InputComponent = React.forwardRef< | |||||||
| 	HTMLInputElement, | 	HTMLInputElement, | ||||||
| 	React.ComponentProps<"input"> | 	React.ComponentProps<"input"> | ||||||
| >(({ className, ...props }, ref) => ( | >(({ className, ...props }, ref) => ( | ||||||
|   <Input | 	<Input className={cn("mx-2", className)} {...props} ref={ref} /> | ||||||
|     className={cn("rounded-e-lg rounded-s-none", className)} |  | ||||||
|     {...props} |  | ||||||
|     ref={ref} |  | ||||||
|   /> |  | ||||||
| )); | )); | ||||||
| InputComponent.displayName = "InputComponent"; | InputComponent.displayName = "InputComponent"; | ||||||
|  |  | ||||||
| @@ -90,7 +86,7 @@ const CountrySelect = ({ | |||||||
| 				<Button | 				<Button | ||||||
| 					type="button" | 					type="button" | ||||||
| 					variant="outline" | 					variant="outline" | ||||||
|           className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10" | 					className="flex gap-1  px-3 focus:z-10" | ||||||
| 					disabled={true} | 					disabled={true} | ||||||
| 				> | 				> | ||||||
| 					<FlagComponent | 					<FlagComponent | ||||||
|   | |||||||
| @@ -1,7 +0,0 @@ | |||||||
| import { phoneNumberClient } from "better-auth/client/plugins"; |  | ||||||
| import { createAuthClient } from "better-auth/react"; |  | ||||||
|  |  | ||||||
| export const authClient = createAuthClient({ |  | ||||||
| 	baseURL: process.env.BETTER_AUTH_URL, |  | ||||||
| 	plugins: [phoneNumberClient()], |  | ||||||
| }); |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| "use server"; | "use server"; | ||||||
| import { auth } from "@/lib/auth"; | import { auth } from "@/app/auth"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use server"; | "use server"; | ||||||
| import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||||
| import { cache } from "react"; | import { cache } from "react"; | ||||||
| import { auth } from "./auth"; | import { auth } from "../app/auth"; | ||||||
|  |  | ||||||
| const getCurrentUserCache = cache(async () => { | const getCurrentUserCache = cache(async () => { | ||||||
| 	const session = await auth.api.getSession({ | 	const session = await auth.api.getSession({ | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								lib/auth.ts
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								lib/auth.ts
									
									
									
									
									
								
							| @@ -1,44 +0,0 @@ | |||||||
| import { sendOtp } from "@/actions/auth-actions"; |  | ||||||
| import { betterAuth } from "better-auth"; |  | ||||||
| import { prismaAdapter } from "better-auth/adapters/prisma"; |  | ||||||
| import { phoneNumber } from "better-auth/plugins"; |  | ||||||
| import prisma from "./db"; |  | ||||||
|  |  | ||||||
| export const auth = betterAuth({ |  | ||||||
| 	session: { |  | ||||||
| 		cookieCache: { |  | ||||||
| 			enabled: true, |  | ||||||
| 			maxAge: 10 * 60, // Cache duration in seconds |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",") || [ |  | ||||||
| 		"localhost:3000", |  | ||||||
| 	], |  | ||||||
| 	user: { |  | ||||||
| 		additionalFields: { |  | ||||||
| 			role: { |  | ||||||
| 				type: "string", |  | ||||||
| 				required: false, |  | ||||||
| 				defaultValue: "USER", |  | ||||||
| 				input: false, // don't allow user to set role |  | ||||||
| 			}, |  | ||||||
| 			lang: { |  | ||||||
| 				type: "string", |  | ||||||
| 				required: false, |  | ||||||
| 				defaultValue: "en", |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	database: prismaAdapter(prisma, { |  | ||||||
| 		provider: "postgresql", // or "mysql", "postgresql", ...etc |  | ||||||
| 	}), |  | ||||||
| 	plugins: [ |  | ||||||
| 		phoneNumber({ |  | ||||||
| 			sendOTP: async ({ phoneNumber, code }) => { |  | ||||||
| 				// Implement sending OTP code via SMS |  | ||||||
| 				console.log("Send OTP in auth.ts", phoneNumber, code); |  | ||||||
| 				await sendOtp(phoneNumber, code); |  | ||||||
| 			}, |  | ||||||
| 		}), |  | ||||||
| 	], |  | ||||||
| }); |  | ||||||
							
								
								
									
										17
									
								
								lib/db.ts
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								lib/db.ts
									
									
									
									
									
								
							| @@ -1,17 +0,0 @@ | |||||||
| import { PrismaClient } from "@prisma/client"; |  | ||||||
|  |  | ||||||
| const prismaClientSingleton = () => { |  | ||||||
| 	return new PrismaClient(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>; |  | ||||||
|  |  | ||||||
| const globalForPrisma = globalThis as unknown as { |  | ||||||
| 	prisma: PrismaClientSingleton | undefined; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); |  | ||||||
|  |  | ||||||
| export default prisma; |  | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; |  | ||||||
							
								
								
									
										38
									
								
								lib/types/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/types/user.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | import type { ISODateString } from "next-auth"; | ||||||
|  |  | ||||||
|  | export interface Permission { | ||||||
|  | 	id: number; | ||||||
|  | 	name: string; | ||||||
|  | 	user: User; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface TAuthUser { | ||||||
|  | 	expiry?: string; | ||||||
|  | 	token?: string; | ||||||
|  | 	user: User; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface User { | ||||||
|  | 	id: number; | ||||||
|  | 	username: string; | ||||||
|  | 	email: string; | ||||||
|  | 	user_permissions: Permission[]; | ||||||
|  | 	first_name: string; | ||||||
|  | 	last_name: string; | ||||||
|  | 	is_superuser: boolean; | ||||||
|  | 	date_joined: string; | ||||||
|  | 	last_login: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Session { | ||||||
|  | 	user?: { | ||||||
|  | 		token?: string; | ||||||
|  | 		name?: string | null; | ||||||
|  | 		email?: string | null; | ||||||
|  | 		image?: string | null; | ||||||
|  | 		user?: User & { | ||||||
|  | 			expiry?: string; | ||||||
|  | 		}; | ||||||
|  | 	}; | ||||||
|  | 	expires: ISODateString; | ||||||
|  | } | ||||||
							
								
								
									
										745
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										745
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -17,7 +17,6 @@ | |||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@faker-js/faker": "^9.3.0", | 		"@faker-js/faker": "^9.3.0", | ||||||
| 		"@hookform/resolvers": "^3.9.1", | 		"@hookform/resolvers": "^3.9.1", | ||||||
| 		"@prisma/client": "^6.1.0", |  | ||||||
| 		"@radix-ui/react-alert-dialog": "^1.1.2", | 		"@radix-ui/react-alert-dialog": "^1.1.2", | ||||||
| 		"@radix-ui/react-checkbox": "^1.1.2", | 		"@radix-ui/react-checkbox": "^1.1.2", | ||||||
| 		"@radix-ui/react-collapsible": "^1.1.1", | 		"@radix-ui/react-collapsible": "^1.1.1", | ||||||
| @@ -31,7 +30,7 @@ | |||||||
| 		"@radix-ui/react-slot": "^1.1.0", | 		"@radix-ui/react-slot": "^1.1.0", | ||||||
| 		"@radix-ui/react-tooltip": "^1.1.4", | 		"@radix-ui/react-tooltip": "^1.1.4", | ||||||
| 		"@tanstack/react-query": "^5.61.4", | 		"@tanstack/react-query": "^5.61.4", | ||||||
| 		"better-auth": "^1.1.13", | 		"axios": "^1.8.4", | ||||||
| 		"class-variance-authority": "^0.7.0", | 		"class-variance-authority": "^0.7.0", | ||||||
| 		"clsx": "^2.1.1", | 		"clsx": "^2.1.1", | ||||||
| 		"cmdk": "^1.0.0", | 		"cmdk": "^1.0.0", | ||||||
| @@ -41,12 +40,12 @@ | |||||||
| 		"moment": "^2.30.1", | 		"moment": "^2.30.1", | ||||||
| 		"motion": "^11.15.0", | 		"motion": "^11.15.0", | ||||||
| 		"next": "15.1.2", | 		"next": "15.1.2", | ||||||
|  | 		"next-auth": "^4.24.11", | ||||||
| 		"next-themes": "^0.4.3", | 		"next-themes": "^0.4.3", | ||||||
| 		"nextjs-toploader": "^3.7.15", | 		"nextjs-toploader": "^3.7.15", | ||||||
| 		"prisma": "^6.1.0", |  | ||||||
| 		"react": "19.0.0", | 		"react": "19.0.0", | ||||||
| 		"react-aria-components": "^1.5.0", | 		"react-aria-components": "^1.5.0", | ||||||
| 		"react-day-picker": "^8.10.1", | 		"react-day-picker": "^9.6.3", | ||||||
| 		"react-dom": "19.0.0", | 		"react-dom": "19.0.0", | ||||||
| 		"react-hook-form": "^7.53.2", | 		"react-hook-form": "^7.53.2", | ||||||
| 		"react-phone-number-input": "^3.4.9", | 		"react-phone-number-input": "^3.4.9", | ||||||
|   | |||||||
| @@ -1,10 +0,0 @@ | |||||||
| "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(); |  | ||||||
| } |  | ||||||
							
								
								
									
										48
									
								
								queries/authentication.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								queries/authentication.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | "use server"; | ||||||
|  | import type { TAuthUser } from "@/lib/types/user"; | ||||||
|  | import axiosInstance from "@/utils/axiosInstance"; | ||||||
|  |  | ||||||
|  | export async function login({ | ||||||
|  | 	password, | ||||||
|  | 	username, | ||||||
|  | }: { | ||||||
|  | 	username: string; | ||||||
|  | 	password: string; | ||||||
|  | }): Promise<TAuthUser> { | ||||||
|  | 	const response = await axiosInstance | ||||||
|  | 		.post("/auth/login/", { | ||||||
|  | 			username: username, | ||||||
|  | 			password: password, | ||||||
|  | 		}) | ||||||
|  | 		.then((res) => { | ||||||
|  | 			console.log(res); | ||||||
|  | 			return res.data; // Return the data from the response | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err.response); | ||||||
|  | 			throw err; // Throw the error to maintain the Promise rejection | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 	return response; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function logout({ token }: { token: string }) { | ||||||
|  | 	const response = await fetch( | ||||||
|  | 		`${process.env.NEXT_PUBLIC_API_URL}/auth/logout/`, | ||||||
|  | 		{ | ||||||
|  | 			method: "POST", | ||||||
|  | 			headers: { | ||||||
|  | 				"Content-Type": "application/json", | ||||||
|  | 				Authorization: `Token ${token}`, // Include the token for authentication | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	); | ||||||
|  |  | ||||||
|  | 	if (response.status !== 204) { | ||||||
|  | 		throw new Error("Failed to log out from the backend"); | ||||||
|  | 	} | ||||||
|  | 	console.log("logout res in backend", response); | ||||||
|  |  | ||||||
|  | 	// 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 | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								utils/axiosInstance.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								utils/axiosInstance.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import axios from "axios"; | ||||||
|  |  | ||||||
|  | axios.defaults.xsrfCookieName = "csrftoken"; | ||||||
|  | axios.defaults.xsrfHeaderName = "X-CSRFToken"; | ||||||
|  |  | ||||||
|  | const axiosInstance = axios.create({ | ||||||
|  | 	baseURL: process.env.NEXT_PUBLIC_API_URL, | ||||||
|  | 	validateStatus: (status) => { | ||||||
|  | 		return status < 500; // Resolve only if the status code is less than 500 | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default axiosInstance; | ||||||
		Reference in New Issue
	
	Block a user