refactor: streamline authentication flow by removing unused code, replacing custom auth utilities with NextAuth, and updating session handling in components
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 5m56s

This commit is contained in:
i701 2025-03-28 22:24:45 +05:00
parent 99c5fef748
commit ef9f032366
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
10 changed files with 291 additions and 323 deletions

View File

@ -10,6 +10,11 @@ const formSchema = z.object({
.regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"), .regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"),
}); });
type FilterUserResponse = {
ok: boolean;
verified: boolean;
};
export async function signin(previousState: ActionState, formData: FormData) { export async function signin(previousState: ActionState, formData: FormData) {
const phoneNumber = formData.get("phoneNumber") as string; const phoneNumber = formData.get("phoneNumber") as string;
const result = formSchema.safeParse({ phoneNumber }); const result = formSchema.safeParse({ phoneNumber });
@ -31,7 +36,28 @@ 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 user = await fetch(
`${process.env.SARLINK_API_BASE_URL}/api/auth/users/filter/?mobile=${FORMATTED_MOBILE_NUMBER}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
const userData = (await user.json()) as FilterUserResponse;
if (!userData?.ok) {
return redirect(`/auth/signup?phone_number=${phoneNumber}`);
}
if (!userData.verified) {
return {
message:
"Your account is on pending verification. Please wait for a response from admin or contact shihaam.",
status: "error",
};
}
const sendOTPResponse = await fetch(
`${process.env.SARLINK_API_BASE_URL}/auth/mobile/`, `${process.env.SARLINK_API_BASE_URL}/auth/mobile/`,
{ {
method: "POST", method: "POST",
@ -43,23 +69,10 @@ export async function signin(previousState: ActionState, formData: FormData) {
}), }),
}, },
); );
const userExists = await userExistsResponse.json(); const otpResponse = await sendOTPResponse.json();
console.log("user exists", userExists); console.log("otpResponse", otpResponse);
if (userExists?.non_field_errors) {
return redirect(`/signup?phone_number=${phoneNumber}`);
}
if (!userExists?.verified) redirect(`/auth/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`);
return {
message:
"Your account is on pending verification. Please wait for a response from admin or contact shihaam.",
status: "error",
};
// await authClient.phoneNumber.sendOtp({
// phoneNumber: NUMBER_WITH_COUNTRY_CODE,
// });
redirect(`/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`);
} }
type ActionState = { type ActionState = {

View File

@ -1,6 +1,5 @@
"use server"; "use server";
import prisma from "@/lib/db";
import type { GroupProfile, MacAddress, OmadaResponse } from "@/lib/types"; import type { GroupProfile, MacAddress, OmadaResponse } from "@/lib/types";
import { formatMacAddress } from "@/lib/utils"; import { formatMacAddress } from "@/lib/utils";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
@ -124,11 +123,11 @@ export async function blockDevice({
if (!macAddress) { if (!macAddress) {
throw new Error("macAddress is a required parameter"); throw new Error("macAddress is a required parameter");
} }
const device = await prisma.device.findFirst({ // const device = await prisma.device.findFirst({
where: { // where: {
mac: macAddress, // mac: macAddress,
}, // },
}); // });
try { try {
const baseUrl: string = process.env.OMADA_BASE_URL || ""; const baseUrl: string = process.env.OMADA_BASE_URL || "";
const url: string = `${baseUrl}/api/v2/sites/${process.env.OMADA_SITE_ID}/cmd/clients/${formatMacAddress(macAddress)}/${type}`; const url: string = `${baseUrl}/api/v2/sites/${process.env.OMADA_SITE_ID}/cmd/clients/${formatMacAddress(macAddress)}/${type}`;
@ -146,16 +145,16 @@ export async function blockDevice({
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
await prisma.device.update({ // await prisma.device.update({
where: { // where: {
id: device?.id, // id: device?.id,
}, // },
data: { // data: {
reasonForBlocking: type === "block" ? reason : "", // reasonForBlocking: type === "block" ? reason : "",
blocked: type === "block", // blocked: type === "block",
blockedBy: blockedBy, // blockedBy: blockedBy,
}, // },
}); // });
revalidatePath("/parental-control"); revalidatePath("/parental-control");
} catch (error) { } catch (error) {
console.error("Error blocking device:", error); console.error("Error blocking device:", error);

View File

@ -1,9 +1,5 @@
import LoginForm from "@/components/auth/login-form"; import LoginForm from "@/components/auth/login-form";
import { auth } from "@/app/auth";
import { headers } from "next/headers";
import Image from "next/image"; import Image from "next/image";
import { redirect } from "next/navigation";
import React from "react";
export default async function LoginPage() { export default async function LoginPage() {
return ( return (

View File

@ -1,12 +1,10 @@
import { authOptions } from "@/app/auth";
import { DevicesTable } from "@/components/devices-table"; import { DevicesTable } from "@/components/devices-table";
import Search from "@/components/search"; import Search from "@/components/search";
import AddDeviceDialogForm from "@/components/user/add-device-dialog"; import AddDeviceDialogForm from "@/components/user/add-device-dialog";
import { getCurrentUser } from "@/lib/auth-utils"; import { getServerSession } from "next-auth";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
export default async function Devices({ export default async function Devices({
searchParams, searchParams,
}: { }: {
@ -18,14 +16,12 @@ export default async function Devices({
}>; }>;
}) { }) {
const query = (await searchParams)?.query || ""; const query = (await searchParams)?.query || "";
const user = await getCurrentUser() const session = await getServerSession(authOptions);
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">My Devices</h3>
My Devices <AddDeviceDialogForm user_id={session?.user?.id} />
</h3>
<AddDeviceDialogForm user_id={user?.id} />
</div> </div>
<div <div
@ -33,7 +29,6 @@ export default async function Devices({
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"
> >
<Search /> <Search />
</div> </div>
<Suspense key={query} fallback={"loading...."}> <Suspense key={query} fallback={"loading...."}>
<DevicesTable parentalControl={false} searchParams={searchParams} /> <DevicesTable parentalControl={false} searchParams={searchParams} />

View File

@ -1,56 +1,54 @@
'use client' "use client";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover" } from "@/components/ui/popover";
import { authClient } from "@/lib/auth-client"; import { Loader2, User as UserIcon } from "lucide-react";
import { Loader2, User as UserIcon } from "lucide-react" import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation";
import { useState } from "react" import { useState } from "react";
export function AccountPopover() { export function AccountPopover() {
const session = authClient.useSession(); const session = useSession();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false);
const router = useRouter() const router = useRouter();
if (session.isPending) { if (session.status === "loading") {
<Button variant={"outline"} disabled> <Button variant={"outline"} disabled>
<Loader2 className="animate-spin" /> <Loader2 className="animate-spin" />
</Button> </Button>;
} }
return ( return (
<Popover>
<Popover> <PopoverTrigger asChild>
<PopoverTrigger asChild> <Button className="w-fit px-2" variant="outline">
<Button className="w-fit px-2" variant="outline"> <UserIcon />
<UserIcon /> </Button>
</Button> </PopoverTrigger>
</PopoverTrigger> <PopoverContent className="w-fit">
<PopoverContent className="w-fit"> <div className="grid gap-4">
<div className="grid gap-4"> <div className="space-y-2">
<div className="space-y-2"> <h4 className="font-medium leading-none">
<h4 className="font-medium leading-none">{session.data?.user?.name}</h4> {session.data?.user?.name}
<p className="text-sm text-muted-foreground"> </h4>
{session.data?.user?.phoneNumber} <p className="text-sm text-muted-foreground">
</p> {session.data?.user?.phoneNumber}
</div> </p>
<Button disabled={loading} onClick={async () => { </div>
setLoading(true) <Button
await authClient.signOut({ disabled={loading}
fetchOptions: { onClick={async () => {
onSuccess: () => { setLoading(true);
router.push("/login"); // redirect to login page await signOut();
}, setLoading(false);
}, }}
}) >
setLoading(false) {loading ? <Loader2 className="animate-spin" /> : "Logout"}
}}> </Button>
{loading ? <Loader2 className="animate-spin" /> : "Logout"} </div>
</Button> </PopoverContent>
</div> </Popover>
</PopoverContent> );
</Popover>
)
} }

View File

@ -4,33 +4,26 @@ import { Wallet } from "@/components/wallet";
import { ModeToggle } from "@/components/theme-toggle"; import { ModeToggle } from "@/components/theme-toggle";
import { AppSidebar } from "@/components/ui/app-sidebar"; import { AppSidebar } from "@/components/ui/app-sidebar";
import { authOptions } from "@/app/auth";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { auth } from "@/app/auth"; import { getServerSession } from "next-auth";
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";
export async function ApplicationLayout({ export async function ApplicationLayout({
children, children,
}: { children: React.ReactNode }) { }: { children: React.ReactNode }) {
const session = await auth.api.getSession({ const session = await getServerSession(authOptions);
headers: await headers(),
});
const billFormula = await prisma.billFormula.findFirst();
const user = await prisma.user.findFirst({
where: {
id: session?.user?.id,
},
});
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar role={session?.user?.role || "USER"} /> <AppSidebar role={"admin"} />
<DeviceCartDrawer billFormula={billFormula || null} /> {/* <DeviceCartDrawer billFormula={billFormula || null} /> */}
<SidebarInset> <SidebarInset>
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10"> <header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10">
<div className="flex items-center gap-2 "> <div className="flex items-center gap-2 ">
@ -44,7 +37,7 @@ export async function ApplicationLayout({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Wallet walletBalance={user?.walletBalance || 0} /> {/* <Wallet walletBalance={user?.walletBalance || 0} /> */}
<ModeToggle /> <ModeToggle />
<AccountPopover /> <AccountPopover />
</div> </div>

View File

@ -3,7 +3,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@ -37,16 +36,16 @@ export default function VerifyOTPForm({
const onSubmit: SubmitHandler<z.infer<typeof OTPSchema>> = (data) => { const onSubmit: SubmitHandler<z.infer<typeof OTPSchema>> = (data) => {
startTransition(async () => { startTransition(async () => {
const isVerified = await authClient.phoneNumber.verify({ // const isVerified = await authClient.phoneNumber.verify({
phoneNumber: phone_number, // phoneNumber: phone_number,
code: data.pin, // code: data.pin,
}); // });
console.log({ isVerified }); // console.log({ isVerified });
if (!isVerified.error) { // if (!isVerified.error) {
router.push("/devices"); // router.push("/devices");
} else { // } else {
toast.error(isVerified.error.message); // toast.error(isVerified.error.message);
} // }
}); });
}; };
@ -70,11 +69,7 @@ export default function VerifyOTPForm({
<p className="text-red-500 text-sm">{errors.pin.message}</p> <p className="text-red-500 text-sm">{errors.pin.message}</p>
)} )}
</div> </div>
<Button <Button className="w-full" disabled={isPending} type="submit">
className="w-full"
disabled={isPending}
type="submit"
>
{isPending ? <Loader2 className="animate-spin" /> : "Login"} {isPending ? <Loader2 className="animate-spin" /> : "Login"}
</Button> </Button>
</div> </div>

View File

@ -1,3 +1,4 @@
import { authOptions } from "@/app/auth";
import { import {
Table, Table,
TableBody, TableBody,
@ -8,9 +9,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { auth } from "@/app/auth"; import { getServerSession } from "next-auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import ClickableRow from "./clickable-row"; import ClickableRow from "./clickable-row";
import DeviceCard from "./device-card"; import DeviceCard from "./device-card";
import Pagination from "./pagination"; import Pagination from "./pagination";
@ -26,85 +25,84 @@ export async function DevicesTable({
}>; }>;
parentalControl?: boolean; parentalControl?: boolean;
}) { }) {
const session = await auth.api.getSession({ const session = await getServerSession(authOptions);
headers: await headers(), const isAdmin = session?.user;
});
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";
const totalDevices = await prisma.device.count({ // const totalDevices = await prisma.device.count({
where: { // where: {
userId: isAdmin ? undefined : session?.session.userId, // userId: isAdmin ? undefined : session?.session.userId,
OR: [ // OR: [
{ // {
name: { // name: {
contains: query || "", // contains: query || "",
mode: "insensitive", // mode: "insensitive",
}, // },
}, // },
{ // {
mac: { // mac: {
contains: query || "", // contains: query || "",
mode: "insensitive", // mode: "insensitive",
}, // },
}, // },
], // ],
NOT: { // NOT: {
payments: { // payments: {
some: { // some: {
paid: false, // paid: false,
}, // },
}, // },
}, // },
isActive: isAdmin ? undefined : parentalControl, // isActive: isAdmin ? undefined : parentalControl,
blocked: isAdmin // blocked: isAdmin
? undefined // ? undefined
: parentalControl !== undefined // : parentalControl !== undefined
? undefined // ? undefined
: false, // : false,
}, // },
}); // });
const totalPages = Math.ceil(totalDevices / 10); // const totalPages = Math.ceil(totalDevices / 10);
const limit = 10; const limit = 10;
const offset = (Number(page) - 1) * limit || 0; const offset = (Number(page) - 1) * limit || 0;
const devices = await prisma.device.findMany({ // const devices = await prisma.device.findMany({
where: { // where: {
userId: session?.session.userId, // userId: session?.session.userId,
OR: [ // OR: [
{ // {
name: { // name: {
contains: query || "", // contains: query || "",
mode: "insensitive", // mode: "insensitive",
}, // },
}, // },
{ // {
mac: { // mac: {
contains: query || "", // contains: query || "",
mode: "insensitive", // mode: "insensitive",
}, // },
}, // },
], // ],
NOT: { // NOT: {
payments: { // payments: {
some: { // some: {
paid: false, // paid: false,
}, // },
}, // },
}, // },
isActive: parentalControl, // isActive: parentalControl,
blocked: parentalControl !== undefined ? undefined : false, // blocked: parentalControl !== undefined ? undefined : false,
}, // },
skip: offset, // skip: offset,
take: limit, // take: limit,
orderBy: { // orderBy: {
name: `${sortBy}` as "asc" | "desc", // name: `${sortBy}` as "asc" | "desc",
}, // },
}); // });
return null;
return ( return (
<div> <div>
{devices.length === 0 ? ( {devices.length === 0 ? (

View File

@ -2,123 +2,119 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Drawer, Drawer,
DrawerClose, DrawerClose,
DrawerContent, DrawerContent,
DrawerDescription, DrawerDescription,
DrawerFooter, DrawerFooter,
DrawerHeader, DrawerHeader,
DrawerTitle, DrawerTitle,
DrawerTrigger, DrawerTrigger,
} from "@/components/ui/drawer"; } from "@/components/ui/drawer";
import { import { WalletDrawerOpenAtom, walletTopUpValue } from "@/lib/atoms";
WalletDrawerOpenAtom,
walletTopUpValue,
} from "@/lib/atoms";
import { authClient } from "@/lib/auth-client";
import type { TopupType } from "@/lib/types"; import type { TopupType } from "@/lib/types";
import { useAtom, } from "jotai"; import { useAtom } from "jotai";
import { import { CircleDollarSign, Loader2, Wallet2 } from "lucide-react";
CircleDollarSign, import { useSession } from "next-auth/react";
Loader2, import { usePathname } from "next/navigation";
Wallet2,
} from "lucide-react";
import { usePathname, } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import NumberInput from "./number-input"; import NumberInput from "./number-input";
export function Wallet({ export function Wallet({
walletBalance, walletBalance,
}: { }: {
walletBalance: number; walletBalance: number;
}) { }) {
const session = authClient.useSession(); const session = useSession();
const pathname = usePathname(); const pathname = usePathname();
const [amount, setAmount] = useAtom(walletTopUpValue); const [amount, setAmount] = useAtom(walletTopUpValue);
const [isOpen, setIsOpen] = useAtom(WalletDrawerOpenAtom); const [isOpen, setIsOpen] = useAtom(WalletDrawerOpenAtom);
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
// const router = useRouter(); // const router = useRouter();
if (pathname === "/payment") { if (pathname === "/payment") {
return null; return null;
} }
const data: TopupType = { const data: TopupType = {
userId: session?.data?.user.id ?? "", userId: session?.data?.user.id ?? "",
amount: Number.parseFloat(amount.toFixed(2)), amount: Number.parseFloat(amount.toFixed(2)),
paid: false, paid: false,
}; };
return ( return (
<Drawer open={isOpen} onOpenChange={setIsOpen}> <Drawer open={isOpen} onOpenChange={setIsOpen}>
<DrawerTrigger asChild> <DrawerTrigger asChild>
<Button onClick={() => setIsOpen(!isOpen)} variant="outline"> <Button onClick={() => setIsOpen(!isOpen)} variant="outline">
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)} MVR {new Intl.NumberFormat("en-US", {
<Wallet2 /> minimumFractionDigits: 2,
</Button> maximumFractionDigits: 2,
</DrawerTrigger> }).format(walletBalance)}{" "}
<DrawerContent> MVR
<div className="mx-auto w-full max-w-sm"> <Wallet2 />
<DrawerHeader> </Button>
<DrawerTitle>Wallet</DrawerTitle> </DrawerTrigger>
<DrawerDescription asChild> <DrawerContent>
<div> <div className="mx-auto w-full max-w-sm">
Your wallet balance is{" "} <DrawerHeader>
<span className="font-semibold"> <DrawerTitle>Wallet</DrawerTitle>
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(walletBalance)} <DrawerDescription asChild>
</span>{" "} <div>
</div> Your wallet balance is{" "}
</DrawerDescription> <span className="font-semibold">
</DrawerHeader> {new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(walletBalance)}
</span>{" "}
</div>
</DrawerDescription>
</DrawerHeader>
<div className="px-4 flex flex-col gap-4"> <div className="px-4 flex flex-col gap-4">
<NumberInput <NumberInput
label="Set amount to top up" label="Set amount to top up"
value={amount} value={amount}
onChange={(value) => setAmount(value)} onChange={(value) => setAmount(value)}
maxAllowed={5000} maxAllowed={5000}
isDisabled={amount === 0} isDisabled={amount === 0}
/> />
</div>
</div> <DrawerFooter>
<DrawerFooter> <Button
<Button onClick={async () => {
onClick={async () => { console.log(data);
console.log(data) setDisabled(true);
setDisabled(true) // const payment = await createPayment(data)
// const payment = await createPayment(data) setDisabled(false);
setDisabled(false) // setMonths(1)
// setMonths(1) // if (payment) {
// if (payment) { // router.push(`/payments/${payment.id}`);
// router.push(`/payments/${payment.id}`); // setIsOpen(!isOpen);
// setIsOpen(!isOpen); // } else {
// } else { // toast.error("Something went wrong.")
// toast.error("Something went wrong.") // }
// } }}
}} className="w-full"
className="w-full" disabled={amount === 0 || disabled}
disabled={amount === 0 || disabled} >
> {disabled ? (
{disabled ? ( <>
<> <Loader2 className="ml-2 animate-spin" />
<Loader2 className="ml-2 animate-spin" /> </>
</> ) : (
) : ( <>
<> Go to payment
Go to payment <CircleDollarSign />
<CircleDollarSign /> </>
</> )}
)} </Button>
</Button> <DrawerClose asChild>
<DrawerClose asChild> <Button variant="outline">Cancel</Button>
<Button variant="outline">Cancel</Button> </DrawerClose>
</DrawerClose> </DrawerFooter>
</DrawerFooter> </div>
</div> </DrawerContent>
</DrawerContent> </Drawer>
</Drawer> );
);
} }

View File

@ -1,15 +0,0 @@
"use server";
import { headers } from "next/headers";
import { cache } from "react";
import { auth } from "../app/auth";
const getCurrentUserCache = cache(async () => {
const session = await auth.api.getSession({
headers: await headers(),
});
return session?.user;
});
export async function getCurrentUser() {
return await getCurrentUserCache();
}