-
Notifications
You must be signed in to change notification settings - Fork 12
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 all 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,38 @@ | ||||||
| # 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 | ||||||
| *.log | ||||||
|
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. Fix potential trailing-whitespace bug in the The Suggested change-*.log
+*.log📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| "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() { | ||
| try { | ||
| // Verify session via API (server-side verified) | ||
| const sessionRes = await fetch("/api/admin/verify-session"); | ||
| if (!sessionRes.ok) { | ||
| router.push("/admin/login"); | ||
| return; | ||
| } | ||
|
|
||
| const sessionData = await sessionRes.json(); | ||
| setAdminEmail(sessionData.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[]) ?? []); | ||
| } | ||
| } catch (err) { | ||
| router.push("/admin/login"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
|
|
||
| load(); | ||
| }, [router]); | ||
|
|
||
| async function handleLogout() { | ||
| await fetch("/api/admin/logout", { method: "POST" }); | ||
| 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,89 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "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(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Session cookie is set server-side, just redirect | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { createAdminClient } from "@/lib/auth-utils"; | ||
|
|
||
| export async function GET(req: NextRequest) { | ||
| try { | ||
| const adminSessionCookie = req.cookies.get("admin_session")?.value; | ||
|
|
||
| if (!adminSessionCookie) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| // Verify the session exists and get admin details | ||
| const supabase = createAdminClient(); | ||
| const { data: admin } = await supabase | ||
| .from("system_admins") | ||
| .select("id, email") | ||
| .eq("id", adminSessionCookie) | ||
| .maybeSingle(); | ||
|
|
||
| if (!admin) { | ||
| return NextResponse.json({ error: "Invalid session" }, { status: 401 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ id: admin.id, email: admin.email }, { status: 200 }); | ||
| } catch (error) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Confirm
*.pemignore scope (may hide required non-secret PEMs).Ignoring all
*.pemis usually intended to avoid committing secrets, but if the repo ever needs to include public certs (or other non-sensitive.pemfiles) you may want explicit allow-exceptions (e.g.,!public-*.pem) or a narrower pattern.If you tell me which PEM files (if any) must be committed, I can suggest precise
!exception patterns.🤖 Prompt for AI Agents