-
Notifications
You must be signed in to change notification settings - Fork 11
Task 2 for Healthpilot.ai is Completed! #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,37 @@ | ||
| # Environment variables | ||
| .env | ||
| .env.local | ||
| .env*.local | ||
| .env.test | ||
|
|
||
| # Dependencies | ||
| node_modules/ | ||
|
|
||
| # Next.js | ||
| .next/ | ||
| out/ | ||
| dist/ | ||
|
|
||
| # Testing | ||
| coverage/ | ||
| .nyc_output/ | ||
|
|
||
| # Vercel & Deployment | ||
| .vercel/ | ||
|
|
||
| # Build tools | ||
| .turbo/ | ||
|
|
||
| # OS | ||
| .DS_Store | ||
| *.pem | ||
| .DS_Store? | ||
| ._* | ||
| .Spotlight-V100 | ||
| .Trashes | ||
| ehthumbs.db | ||
|
|
||
| # Logs | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| *.pem |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { supabase } from "@/lib/supabase"; | ||
|
|
||
| type Appointment = { | ||
| id: string; | ||
| status: "active" | "done" | "cancelled"; | ||
| created_at: string; | ||
| patients: { name: string; email: string }; | ||
| doctors: { name: string; specialty: string }; | ||
| slots: { start_time: string; end_time: string }; | ||
| }; | ||
|
|
||
| function formatDateTime(iso: string) { | ||
| return new Date(iso).toLocaleString("en-IN", { | ||
| dateStyle: "medium", | ||
| timeStyle: "short", | ||
| }); | ||
| } | ||
|
|
||
| function getStatusColor(status: string) { | ||
| switch (status) { | ||
| case "active": | ||
| return "bg-blue-100 text-blue-700"; | ||
| case "done": | ||
| return "bg-green-100 text-green-700"; | ||
| case "cancelled": | ||
| return "bg-red-100 text-red-700"; | ||
| default: | ||
| return "bg-gray-100 text-gray-700"; | ||
| } | ||
| } | ||
|
|
||
| export default function AdminDashboard() { | ||
| const router = useRouter(); | ||
| const [appointments, setAppointments] = useState<Appointment[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [adminEmail, setAdminEmail] = useState(""); | ||
|
|
||
| useEffect(() => { | ||
| async function load() { | ||
| // Check if admin is logged in | ||
| const email = localStorage.getItem("adminEmail"); | ||
| if (!email) { | ||
| router.push("/admin/login"); | ||
| return; | ||
| } | ||
|
|
||
| setAdminEmail(email); | ||
|
|
||
| // Fetch all appointments | ||
| const { data: apptData, error } = await supabase | ||
| .from("appointments") | ||
| .select( | ||
| "id, status, created_at, patients(name, email), doctors(name, specialty), slots(start_time, end_time)" | ||
| ) | ||
| .order("created_at", { ascending: false }); | ||
|
|
||
| if (!error) { | ||
| setAppointments((apptData as Appointment[]) ?? []); | ||
| } | ||
| setLoading(false); | ||
| } | ||
|
|
||
| load(); | ||
| }, [router]); | ||
|
|
||
| async function handleLogout() { | ||
| await fetch("/api/admin/logout", { method: "POST" }); | ||
| localStorage.removeItem("adminEmail"); | ||
| localStorage.removeItem("adminId"); | ||
| router.push("/admin/login"); | ||
| } | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <main className="flex min-h-screen items-center justify-center"> | ||
| <p className="text-gray-500">Loading...</p> | ||
| </main> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <main className="min-h-screen bg-gray-50 p-6"> | ||
| <div className="mx-auto max-w-6xl"> | ||
| <div className="mb-6 flex items-center justify-between"> | ||
| <div> | ||
| <h1 className="text-3xl font-bold">Admin Dashboard</h1> | ||
| <p className="text-sm text-gray-500">Logged in as: {adminEmail}</p> | ||
| </div> | ||
| <button | ||
| onClick={handleLogout} | ||
| className="rounded-lg border px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" | ||
| > | ||
| Logout | ||
| </button> | ||
| </div> | ||
|
|
||
| <section> | ||
| <h2 className="mb-3 text-lg font-semibold">All Appointments</h2> | ||
| {appointments.length === 0 ? ( | ||
| <p className="text-sm text-gray-500">No appointments found.</p> | ||
| ) : ( | ||
| <div className="overflow-hidden rounded-xl border bg-white"> | ||
| <table className="w-full text-sm"> | ||
| <thead className="bg-gray-50 text-left text-gray-600"> | ||
| <tr> | ||
| <th className="px-4 py-3 font-medium">Patient</th> | ||
| <th className="px-4 py-3 font-medium">Doctor</th> | ||
| <th className="px-4 py-3 font-medium">Specialty</th> | ||
| <th className="px-4 py-3 font-medium">Date & Time</th> | ||
| <th className="px-4 py-3 font-medium">Status</th> | ||
| <th className="px-4 py-3 font-medium">Booked On</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody className="divide-y"> | ||
| {appointments.map((appt) => ( | ||
| <tr key={appt.id}> | ||
| <td className="px-4 py-3"> | ||
| <div className="font-medium">{appt.patients?.name}</div> | ||
| <div className="text-xs text-gray-500"> | ||
| {appt.patients?.email} | ||
| </div> | ||
| </td> | ||
| <td className="px-4 py-3">{appt.doctors?.name}</td> | ||
| <td className="px-4 py-3 text-gray-500"> | ||
| {appt.doctors?.specialty} | ||
| </td> | ||
| <td className="px-4 py-3"> | ||
| {formatDateTime(appt.slots?.start_time)} —{" "} | ||
| {new Date(appt.slots?.end_time).toLocaleTimeString( | ||
| "en-IN", | ||
| { timeStyle: "short" } | ||
| )} | ||
| </td> | ||
| <td className="px-4 py-3"> | ||
| <span | ||
| className={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor( | ||
| appt.status | ||
| )}`} | ||
| > | ||
| {appt.status.charAt(0).toUpperCase() + | ||
| appt.status.slice(1)} | ||
| </span> | ||
| </td> | ||
| <td className="px-4 py-3 text-sm text-gray-500"> | ||
| {formatDateTime(appt.created_at)} | ||
| </td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| )} | ||
| </section> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from "next/navigation"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Link from "next/link"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function AdminLogin() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [email, setEmail] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [password, setPassword] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleSubmit(e: React.FormEvent) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch("/api/admin/login", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ email, password }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(data.error || "Login failed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem("adminEmail", data.admin.email); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem("adminId", data.admin.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.push("/admin/dashboard"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError("An error occurred. Please try again."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <main className="flex min-h-screen items-center justify-center bg-gray-50"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-full max-w-md rounded-xl border bg-white p-8 shadow-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="mb-6 text-2xl font-bold">Admin Login</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form onSubmit={handleSubmit} className="flex flex-col gap-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="mb-1 block text-sm font-medium text-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={email} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="admin@test.com" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="mb-1 block text-sm font-medium text-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Password | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="password" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={password} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setPassword(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full rounded-lg border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Associate each label with its input. These labels are not programmatically connected to the corresponding fields, so assistive tech users lose the field names and clicking the label will not focus the input. ♿ Proposed fix- <label className="mb-1 block text-sm font-medium text-gray-700">
+ <label
+ htmlFor="admin-email"
+ className="mb-1 block text-sm font-medium text-gray-700"
+ >
Email
</label>
<input
+ id="admin-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
@@
- <label className="mb-1 block text-sm font-medium text-gray-700">
+ <label
+ htmlFor="admin-password"
+ className="mb-1 block text-sm font-medium text-gray-700"
+ >
Password
</label>
<input
+ id="admin-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {error && <p className="text-sm text-red-600">{error}</p>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={loading} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="rounded-lg bg-purple-600 py-2 font-medium text-white hover:bg-purple-700 disabled:opacity-50" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {loading ? "Logging in..." : "Login"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-4 text-center text-sm text-gray-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Link href="/" className="text-purple-600 hover:underline"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ← Back to home | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { badRequestResponse, unauthorizedResponse } from "@/lib/auth-utils"; | ||
| import { validateAdminLogin } from "@/lib/validators/admin.validator"; | ||
| import { adminService } from "@/lib/services/admin.service"; | ||
|
|
||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const body = await req.json(); | ||
| const { email, password } = validateAdminLogin(body); | ||
|
|
||
| const admin = await adminService.loginAdmin(email, password); | ||
|
|
||
| const res = NextResponse.json({ success: true, admin }); | ||
| res.cookies.set("admin_session", admin.id, { | ||
| httpOnly: true, | ||
| sameSite: "lax", | ||
| path: "/", | ||
| maxAge: 60 * 60 * 8, | ||
| }); | ||
| return res; | ||
| } catch (error: any) { | ||
| console.error("Error logging in admin:", error); | ||
|
|
||
| if (error.message.includes("required")) { | ||
| return badRequestResponse(error.message); | ||
| } | ||
|
|
||
| if (error.message.includes("Invalid")) { | ||
| return unauthorizedResponse(); | ||
| } | ||
|
Comment on lines
+24
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error handling logic relies on string matching against the error message ( A more robust approach is to use custom error classes (e.g., This pattern is repeated in other API routes as well, such as |
||
|
|
||
| return unauthorizedResponse(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| export async function POST(_req: NextRequest) { | ||
| const res = NextResponse.json({ success: true }); | ||
| res.cookies.delete("admin_session"); | ||
| return res; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,51 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { | ||
| extractBearerToken, | ||
| verifyUserAuth, | ||
| unauthorizedResponse, | ||
| badRequestResponse, | ||
| errorResponse, | ||
| } from "@/lib/auth-utils"; | ||
| import { validateBookAppointment } from "@/lib/validators/appointment.validator"; | ||
| import { appointmentService } from "@/lib/services/appointment.service"; | ||
|
|
||
| export async function POST(_req: NextRequest) { | ||
| return NextResponse.json({ error: "Not implemented" }, { status: 501 }); | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| // Validate input | ||
| const body = await req.json(); | ||
| const { slotId, doctorId } = validateBookAppointment(body); | ||
|
|
||
| // Verify authentication | ||
| const token = extractBearerToken(req); | ||
| if (!token) return unauthorizedResponse(); | ||
|
|
||
| const { user, error: authError } = await verifyUserAuth(token); | ||
| if (authError || !user) return unauthorizedResponse(); | ||
|
|
||
| // Book appointment | ||
| const result = await appointmentService.bookAppointment(user.id, slotId, doctorId); | ||
|
|
||
| return NextResponse.json(result, { status: 201 }); | ||
| } catch (error: any) { | ||
| console.error("Error booking appointment:", error); | ||
|
|
||
| if (error.message.includes("required")) { | ||
| return badRequestResponse(error.message); | ||
| } | ||
|
|
||
| if (error.message.includes("Unauthorized")) { | ||
| return unauthorizedResponse(); | ||
| } | ||
|
|
||
| if (error.message.includes("not found")) { | ||
| return badRequestResponse(error.message); | ||
| } | ||
|
|
||
| if (error.message.includes("already")) { | ||
| return badRequestResponse(error.message); | ||
| } | ||
|
|
||
| return errorResponse(error.message); | ||
| } | ||
| } | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.