From 0322bee56758c004c91589cffa25200ed5843ec7 Mon Sep 17 00:00:00 2001 From: i701 Date: Wed, 27 Nov 2024 14:18:17 +0500 Subject: [PATCH] Refactor authentication and dashboard components - Updated login and signup pages to include session checks and redirection based on user authentication status. - Introduced QueryProvider for managing server state in the application. - Enhanced user experience by integrating session management in the devices and payments dashboard. - Added new user management features with role-based access control in the sidebar. - Created new components for user devices and payments, improving the overall structure and maintainability of the dashboard. - Implemented a table component for better data presentation in user-related views. --- app/(auth)/login/page.tsx | 34 +- app/(auth)/signup/page.tsx | 62 +++- app/(auth)/verify-otp/page.tsx | 40 ++- app/(dashboard)/devices/page.tsx | 16 +- app/(dashboard)/payments/page.tsx | 23 +- app/(dashboard)/user-devices/page.tsx | 5 + app/(dashboard)/user-payments/page.tsx | 11 + app/(dashboard)/users/page.tsx | 56 ++++ app/layout.tsx | 13 +- components/auth/application-layout.tsx | 19 +- components/auth/login-form.tsx | 15 +- components/auth/signup-form.tsx | 442 +++++++++++++------------ components/auth/verify-otp-form.tsx | 140 ++++---- components/query-provider.tsx | 12 + components/ui/app-sidebar.tsx | 77 ++++- components/ui/table.tsx | 120 +++++++ 16 files changed, 713 insertions(+), 372 deletions(-) create mode 100644 app/(dashboard)/user-devices/page.tsx create mode 100644 app/(dashboard)/user-payments/page.tsx create mode 100644 app/(dashboard)/users/page.tsx create mode 100644 components/query-provider.tsx create mode 100644 components/ui/table.tsx diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 4f10988..2c2cc74 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,19 +1,29 @@ import LoginForm from "@/components/auth/login-form"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; import Image from "next/image"; +import { redirect } from "next/navigation"; import React from "react"; -export default function LoginPage() { - return
-
- Sar Link Logo -
- -

SAR Link Portal

-

Pay for your devices and track your bills.

+export default async function LoginPage() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + if (session) { + return redirect("/devices"); + } + return ( +
+
+ Sar Link Logo +
+

SAR Link Portal

+

+ Pay for your devices and track your bills. +

+
+
-
-
; + ); } - - diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index 30268e3..315300e 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,25 +1,51 @@ import SignUpForm from "@/components/auth/signup-form"; +import { auth } from "@/lib/auth"; import prisma from "@/lib/db"; +import { headers } from "next/headers"; import Image from "next/image"; +import { redirect } from "next/navigation"; import React from "react"; -export default async function LoginPage() { - const atolls = await prisma.atoll.findMany({ - include: { - islands: true - } - }) - return
-
- Sar Link Logo -
+export default async function SignupPage({ + searchParams, +}: { + searchParams: Promise<{ phone_number: string }>; +}) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + if (session) { + return redirect("/devices"); + } -

SAR Link Portal

-

Pay for your devices and track your bills.

-
- -
-
; + const phone_number = (await searchParams).phone_number; + if (!phone_number) { + return redirect("/login"); + } + const atolls = await prisma.atoll.findMany({ + include: { + islands: true, + }, + }); + + return ( +
+
+ Sar Link Logo +
+

SAR Link Portal

+

+ Pay for your devices and track your bills. +

+
+ +
+
+ ); } - - diff --git a/app/(auth)/verify-otp/page.tsx b/app/(auth)/verify-otp/page.tsx index 8a53726..79f1221 100644 --- a/app/(auth)/verify-otp/page.tsx +++ b/app/(auth)/verify-otp/page.tsx @@ -1,24 +1,34 @@ import VerifyOTPForm from "@/components/auth/verify-otp-form"; import Image from "next/image"; +import { redirect } from "next/navigation"; import React from "react"; export default async function VerifyOTP({ - searchParams, + searchParams, }: { - searchParams: Promise<{ phone_number: string }> + searchParams: Promise<{ phone_number: string }>; }) { - const phone_number = (await searchParams).phone_number - return
-
- Sar Link Logo -
+ const phone_number = (await searchParams).phone_number; + if (!phone_number) { + return redirect("/login"); + } + console.log( + "phone number from server page params (verify otp page)", + phone_number, + ); -

SAR Link Portal

-

Pay for your devices and track your bills.

-
- -
-
; + return ( +
+
+ Sar Link Logo +
+

SAR Link Portal

+

+ Pay for your devices and track your bills. +

+
+ +
+
+ ); } - - diff --git a/app/(dashboard)/devices/page.tsx b/app/(dashboard)/devices/page.tsx index eeae826..028e529 100644 --- a/app/(dashboard)/devices/page.tsx +++ b/app/(dashboard)/devices/page.tsx @@ -1,5 +1,15 @@ +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + export default async function Devices() { - return
-

Devices

-
; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return ( +
+

Server session

+
{JSON.stringify(session?.user, null, 2)}
+
+ ); } diff --git a/app/(dashboard)/payments/page.tsx b/app/(dashboard)/payments/page.tsx index 90bbe8d..0d3e249 100644 --- a/app/(dashboard)/payments/page.tsx +++ b/app/(dashboard)/payments/page.tsx @@ -1,17 +1,14 @@ -'use client' -import { PhoneInput } from '@/components/ui/phone-input' -import React from 'react' +"use client"; +import { authClient } from "@/lib/auth-client"; +import React from "react"; export default function MyPayments() { - return ( -
- -
+ const session = authClient.useSession(); - ) + return ( +
+

Client session

+
{JSON.stringify(session.data, null, 2)}
+
+ ); } diff --git a/app/(dashboard)/user-devices/page.tsx b/app/(dashboard)/user-devices/page.tsx new file mode 100644 index 0000000..f874294 --- /dev/null +++ b/app/(dashboard)/user-devices/page.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function UserDevices() { + return
UserDevices
; +} diff --git a/app/(dashboard)/user-payments/page.tsx b/app/(dashboard)/user-payments/page.tsx new file mode 100644 index 0000000..0b2ccae --- /dev/null +++ b/app/(dashboard)/user-payments/page.tsx @@ -0,0 +1,11 @@ +import { AdminAuthGuard } from "@/lib/auth-guard"; +import React from "react"; + +export default async function UserPayments() { + await AdminAuthGuard(); + return ( +
+

User Payments

+
+ ); +} diff --git a/app/(dashboard)/users/page.tsx b/app/(dashboard)/users/page.tsx new file mode 100644 index 0000000..a69f105 --- /dev/null +++ b/app/(dashboard)/users/page.tsx @@ -0,0 +1,56 @@ +import Filter from "@/components/filter"; +import Search from "@/components/search"; +import { UsersTable } from "@/components/user-table"; +import { AdminAuthGuard } from "@/lib/auth-guard"; +import { CheckCheck, Hourglass, Minus } from "lucide-react"; +import React, { Suspense } from "react"; +export default async function AdminUsers({ + searchParams, +}: { + searchParams: Promise<{ + query: string; + page: number; + sortBy: string; + status: string; + }>; +}) { + await AdminAuthGuard(); + + return ( +
+

+ Users +

+
+ + , + }, + { + value: "unverified", + label: "Unverfieid", + icon: , + }, + { + value: "verified", + label: "Verified", + icon: , + }, + ]} + defaultOption="all" + queryParamKey="status" + /> +
+ + + +
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 7d0e3f9..4de9a08 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,10 +1,11 @@ -import type { Metadata } from "next"; -import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; -import { Barlow } from "next/font/google"; -import NextTopLoader from 'nextjs-toploader'; -import { Toaster } from 'sonner' +import type { Metadata } from "next"; +import { Barlow } from "next/font/google"; +import NextTopLoader from "nextjs-toploader"; +import { Toaster } from "sonner"; +import "./globals.css"; +import QueryProvider from "@/components/query-provider"; const barlow = Barlow({ subsets: ["latin"], weight: ["100", "300", "400", "500", "600", "700", "800", "900"], @@ -32,7 +33,7 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - {children} + {children} diff --git a/components/auth/application-layout.tsx b/components/auth/application-layout.tsx index 4d66be2..2675cad 100644 --- a/components/auth/application-layout.tsx +++ b/components/auth/application-layout.tsx @@ -18,24 +18,24 @@ import { auth } from "@/lib/auth"; import { headers } from "next/headers"; import { AccountPopover } from "./account-popver"; -export async function ApplicationLayout({ children }: { children: React.ReactNode }) { +export async function ApplicationLayout({ + children, +}: { children: React.ReactNode }) { const session = await auth.api.getSession({ headers: await headers(), // you need to pass the headers object. }); return ( - + -
-
+
+
- - MENU - + MENU @@ -50,10 +50,7 @@ export async function ApplicationLayout({ children }: { children: React.ReactNod
-
- - {children} -
+
{children}
); diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 8b407fe..f7d86de 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -1,6 +1,5 @@ "use client"; - import { Button } from "@/components/ui/button"; import { signin } from "@/actions/auth-actions"; @@ -15,13 +14,17 @@ export default function LoginForm() { }); return ( -
-
+ +
@@ -29,7 +32,11 @@ export default function LoginForm() { {state.status === "error" && (

{state.message}

)} -
diff --git a/components/auth/signup-form.tsx b/components/auth/signup-form.tsx index bd4beef..a97eb5d 100644 --- a/components/auth/signup-form.tsx +++ b/components/auth/signup-form.tsx @@ -8,234 +8,244 @@ import { cn } from "@/lib/utils"; import { Loader } from "lucide-react"; import { useActionState } from "react"; import { useSearchParams } from "next/navigation"; -import { Atoll, Island, Prisma } from "@prisma/client"; -import * as React from "react" +import type { Island, Prisma } from "@prisma/client"; +import * as React from "react"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; type AtollWithIslands = Prisma.AtollGetPayload<{ - include: { - islands: true; - } -}> + include: { + islands: true; + }; +}>; export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) { - const [atoll, setAtoll] = React.useState() - const [island, setIsland] = React.useState() + const [atoll, setAtoll] = React.useState(); + const [islands, setIslands] = React.useState(); + React.useEffect(() => { + setIslands(atoll?.islands); + }, [atoll]); + const [actionState, action, isPending] = useActionState(signup, { + message: "", + }); + const params = useSearchParams(); + const phoneNumberFromUrl = params.get("phone_number"); + const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join(""); - const [actionState, action, isPending] = useActionState(signup, { - message: "", - }); - const params = useSearchParams(); - const phoneNumberFromUrl = params.get("phone_number"); - const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("") + return ( + +
+
+ + + {actionState.errors?.fieldErrors.name && ( + + {actionState.errors?.fieldErrors.name} + + )} +
+
+ + + {actionState?.errors?.fieldErrors?.id_card?.[0] && ( + + {actionState.errors.fieldErrors.id_card[0]} + + )} + {actionState.db_error === "id_card" && ( + + {actionState.message} + + )} +
+
+
+ + +
+
+ + +
+
- return ( - -
-
- +
+ + + {actionState.errors?.fieldErrors?.house_name && ( + + {actionState.errors?.fieldErrors?.house_name} + + )} +
- - {actionState.errors?.fieldErrors.name && ( - - {actionState.errors?.fieldErrors.name} - - )} -
-
- - - {actionState?.errors?.fieldErrors?.id_card?.[0] && ( - - {actionState.errors.fieldErrors.id_card[0]} - - )} - {actionState.db_error === "id_card" && ( - - {actionState.message} - - )} -
-
- {/* - - {actionState?.errors?.fieldErrors.island_name && ( - - {actionState?.errors?.fieldErrors.island_name} - - )} */} +
+ + + {actionState.errors?.fieldErrors?.dob && ( + + {actionState.errors?.fieldErrors?.dob} + + )} +
-
- - -
-
- - -
-
+
+ + +
+ {actionState?.errors?.fieldErrors?.phone_number?.[0] && ( + + {actionState.errors.fieldErrors.phone_number[0]} + + )} + {actionState.db_error === "phone_number" && ( + + {actionState.message} + + )} + +
-
- - - {actionState.errors?.fieldErrors?.house_name && ( - - {actionState.errors?.fieldErrors?.house_name} - - )} -
- -
- - - {actionState.errors?.fieldErrors?.dob && ( - - {actionState.errors?.fieldErrors?.dob} - - )} -
- -
- - -
- {actionState?.errors?.fieldErrors?.phone_number?.[0] && ( - - {actionState.errors.fieldErrors.phone_number[0]} - - )} - -
- -
- Already have an account?{" "} - - login - -
- - - ); +
+ Already have an account?{" "} + + login + +
+ + ); } - - - - - - - - diff --git a/components/auth/verify-otp-form.tsx b/components/auth/verify-otp-form.tsx index 9280727..68253b4 100644 --- a/components/auth/verify-otp-form.tsx +++ b/components/auth/verify-otp-form.tsx @@ -4,84 +4,86 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; 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 { Loader } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useTransition } from "react"; import { type SubmitHandler, useForm } from "react-hook-form"; -import { toast } from 'sonner' +import { toast } from "sonner"; import { z } from "zod"; const OTPSchema = z.object({ - pin: z.string().min(6, { - message: "Your one-time password must be 6 characters.", - }), + pin: z.string().min(6, { + message: "Your one-time password must be 6 characters.", + }), }); -export default function VerifyOTPForm({ phone_number }: { phone_number: string }) { - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - console.log("verification in OTP form", phone_number) - const { - register, - handleSubmit, - formState: { errors }, - } = useForm>({ - defaultValues: { - pin: "", - }, - resolver: zodResolver(OTPSchema), - }); +export default function VerifyOTPForm({ + phone_number, +}: { phone_number: string }) { + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + console.log("verification in OTP form", phone_number); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm>({ + defaultValues: { + pin: "", + }, + resolver: zodResolver(OTPSchema), + }); - const onSubmit: SubmitHandler> = (data) => { + const onSubmit: SubmitHandler> = (data) => { + startTransition(async () => { + const isVerified = await authClient.phoneNumber.verify({ + phoneNumber: phone_number, + code: data.pin, + }); + console.log({ isVerified }); + if (!isVerified.error) { + router.push("/devices"); + } else { + toast.error(isVerified.error.message); + } + }); + }; - startTransition(async () => { - const isVerified = await authClient.phoneNumber.verify({ - phoneNumber: phone_number, - code: data.pin, - }); - console.log({ isVerified }); - if (!isVerified.error) { - router.push("/devices"); - } else { - toast.error(isVerified.error.message); - } - }); - } - - return ( -
-
-
- - - {errors.pin && ( -

{errors.pin.message}

- )} -
- -
-
- Go back to{" "} - - login - -
-
- ); + return ( +
+
+
+ + + {errors.pin && ( +

{errors.pin.message}

+ )} +
+ +
+
+ Go back to{" "} + + login + +
+
+ ); } diff --git a/components/query-provider.tsx b/components/query-provider.tsx new file mode 100644 index 0000000..d7e386d --- /dev/null +++ b/components/query-provider.tsx @@ -0,0 +1,12 @@ +"use client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +export default function QueryProvider({ + children, +}: { children: React.ReactNode }) { + return ( + {children} + ); +} diff --git a/components/ui/app-sidebar.tsx b/components/ui/app-sidebar.tsx index 205d5ec..bc2fede 100644 --- a/components/ui/app-sidebar.tsx +++ b/components/ui/app-sidebar.tsx @@ -24,6 +24,7 @@ const data = { { title: "MENU", url: "#", + requiredRoles: ["ADMIN", "USER"], items: [ { title: "Devices", @@ -35,13 +36,34 @@ const data = { }, ], }, - + { + title: "ADMIN CONTROL", + url: "#", + requiredRoles: ["ADMIN"], + items: [ + { + title: "Users", + url: "/users", + }, + { + title: "User Devices", + url: "/user-devices", + }, + { + title: "User Payments", + url: "/user-payments", + }, + ], + }, ], }; -export function AppSidebar({ ...props }: React.ComponentProps) { +export function AppSidebar({ + role, + ...props +}: React.ComponentProps & { role: string }) { return ( - +

Sar Link Portal @@ -49,7 +71,8 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {/* We create a collapsible SidebarGroup for each parent. */} - {data.navMain.map((item) => ( + {/* {data.navMain.map((item) => ( + ) { - ))} + ))} */} + {data.navMain + .filter( + (item) => + !item.requiredRoles || item.requiredRoles.includes(role || ""), + ) + .map((item) => { + if (item.requiredRoles?.includes(role)) { + return ( + + + + + {item.title}{" "} + + + + + + + {item.items.map((item) => ( + + + + {item.title} + + + + ))} + + + + + + ); + } + })} diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}