Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions app/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import { useEffect, useState } from "react";

type AppointmentRow = {
id: string;
status: string;
patients?: {
name: string;
};
doctors?: {
name: string;
};
slots?: {
start_time: string;
};
};

export default function AdminDashboard() {
const [rows, setRows] = useState<AppointmentRow[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const ok = localStorage.getItem("adminLoggedIn");

if (ok !== "true") {
window.location.href = "/admin/login";
return;
}
Comment thread
DHIRAVIYASUNDARAM marked this conversation as resolved.

loadAppointments();
}, []);

async function loadAppointments() {
try {
const res = await fetch("/api/admin/appointments");
const data = await res.json();

setRows(data || []);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
Comment on lines +34 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the response before storing it in rows.

loadAppointments() assumes every JSON body is an array. Once the API starts returning proper 401/500 payloads, setRows(data || []) can store an object, and the render path later calls rows.map(...) on a non-array value.

Suggested fix
+  const [error, setError] = useState("");
+
   async function loadAppointments() {
     try {
       const res = await fetch("/api/admin/appointments");
       const data = await res.json();
-
-      setRows(data || []);
+      if (!res.ok || !Array.isArray(data)) {
+        throw new Error("Failed to load appointments");
+      }
+      setRows(data);
     } catch (error) {
-      console.log(error);
+      setError("Failed to load appointments");
+      setRows([]);
     } finally {
       setLoading(false);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/admin/dashboard/page.tsx` around lines 34 - 44, In loadAppointments(),
validate the fetch response and the parsed JSON before calling setRows: first
check res.ok and handle non-2xx responses (e.g., setRows([]) and surface/log the
error or handle 401 specially), then ensure the parsed body is an array using
Array.isArray(data) and only call setRows(data) when it is; if it’s not an
array, log the unexpected payload (include status and body) and call setRows([])
to avoid later rows.map errors. Ensure these checks occur in the try block
before setRows and preserve the existing setLoading(false) in finally.

}

function handleLogout() {
localStorage.removeItem("adminLoggedIn");
window.location.href = "/";
}

return (
<main className="min-h-screen bg-gray-50 p-8">
<div className="mx-auto max-w-6xl">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold">All Appointments</h1>

<button
onClick={handleLogout}
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Logout
</button>
</div>

{loading ? (
<p>Loading...</p>
) : (
<table className="w-full border bg-white text-sm">
<thead>
<tr className="bg-gray-100">
<th className="border p-2">Patient</th>
<th className="border p-2">Doctor</th>
<th className="border p-2">Slot</th>
<th className="border p-2">Status</th>
</tr>
</thead>

<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={4} className="p-4 text-center">
No appointments found
</td>
</tr>
) : (
rows.map((row) => (
<tr key={row.id}>
<td className="border p-2">{row.patients?.name}</td>
<td className="border p-2">{row.doctors?.name}</td>
<td className="border p-2">{row.slots?.start_time}</td>
<td className="border p-2">{row.status}</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
</main>
);
}
69 changes: 69 additions & 0 deletions app/admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { useState } from "react";

export default function AdminLogin() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");

async function handleLogin(e: React.FormEvent) {
e.preventDefault();
setError("");

const res = await fetch("/api/admin/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});

const data = await res.json();

if (res.ok) {
localStorage.setItem("adminLoggedIn", "true");
window.location.href = "/admin/dashboard";
} else {
setError(data.error || "Login failed");
Comment on lines +14 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle request and JSON parse failures in the submit path.

If fetch() rejects, or the server returns a non-JSON error body, this function throws before setError(...) runs. The login flow then fails with no feedback to the user.

Suggested fix
   async function handleLogin(e: React.FormEvent) {
     e.preventDefault();
     setError("");
-
-    const res = await fetch("/api/admin/login", {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({ email, password }),
-    });
-
-    const data = await res.json();
-
-    if (res.ok) {
-      localStorage.setItem("adminLoggedIn", "true");
-      window.location.href = "/admin/dashboard";
-    } else {
-      setError(data.error || "Login failed");
+    try {
+      const res = await fetch("/api/admin/login", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({ email, password }),
+      });
+
+      const data = await res.json().catch(() => ({}));
+
+      if (res.ok) {
+        localStorage.setItem("adminLoggedIn", "true");
+        window.location.href = "/admin/dashboard";
+      } else {
+        setError(data.error || "Login failed");
+      }
+    } catch {
+      setError("Login failed");
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/admin/login/page.tsx` around lines 14 - 28, The submit path currently
assumes fetch succeeds and the response is JSON; wrap the network call and
parsing in a try/catch around the fetch(...) and res.json() so rejections or
non-JSON bodies don't throw and leave the user without feedback. Specifically,
in the login submit handler that calls fetch("/api/admin/login"), catch network
errors and call setError(...) with a friendly message, and when res.ok is false
attempt to parse JSON in a guarded way (fall back to res.text() if JSON.parse
fails) and then call setError(data.error || text || "Login failed"); keep the
existing success actions (localStorage.setItem("adminLoggedIn", "true") and
window.location.href = "/admin/dashboard") only when res.ok and parsing
succeeds.

}
}

return (
<main className="flex min-h-screen items-center justify-center bg-gray-50">
<form
onSubmit={handleLogin}
className="w-full max-w-md rounded-xl border bg-white p-8"
>
<h1 className="mb-6 text-3xl font-bold">Admin Login</h1>

<input
type="email"
placeholder="Email"
className="mb-4 w-full rounded border px-4 py-3"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>

<input
type="password"
placeholder="Password"
className="mb-4 w-full rounded border px-4 py-3"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>

{error && (
<p className="mb-4 text-sm text-red-600">{error}</p>
)}

<button
type="submit"
className="w-full rounded bg-black py-3 text-white"
>
Login
</button>
</form>
</main>
);
}
31 changes: 31 additions & 0 deletions app/api/admin/appointments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function GET() {
Comment thread
DHIRAVIYASUNDARAM marked this conversation as resolved.
try {
const { data, error } = await supabase
.from("appointments")
.select(`
id,
status,
created_at,
patients(name),
doctors(name),
slots(start_time)
`)
.order("created_at", { ascending: false });

if (error) {
return NextResponse.json([], { status: 200 });
}

return NextResponse.json(data || []);
} catch (error) {
return NextResponse.json([], { status: 200 });
Comment on lines +23 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not convert database failures into 200 [].

Both failure paths currently masquerade as an empty dashboard, so a broken query or missing env var becomes “No appointments found” instead of an actionable server error. Return a 5xx here and let the client render an error state.

Suggested fix
     if (error) {
-      return NextResponse.json([], { status: 200 });
+      return NextResponse.json(
+        { error: "Failed to load appointments" },
+        { status: 500 }
+      );
     }
 
     return NextResponse.json(data || []);
   } catch (error) {
-    return NextResponse.json([], { status: 200 });
+    return NextResponse.json(
+      { error: "Failed to load appointments" },
+      { status: 500 }
+    );
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error) {
return NextResponse.json([], { status: 200 });
}
return NextResponse.json(data || []);
} catch (error) {
return NextResponse.json([], { status: 200 });
if (error) {
return NextResponse.json(
{ error: "Failed to load appointments" },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
return NextResponse.json(
{ error: "Failed to load appointments" },
{ status: 500 }
);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/admin/appointments/route.ts` around lines 23 - 29, The code is
turning DB/query failures into a 200 with an empty array; update both error
paths to return a 5xx response and include the error information so the client
can show an error state. Specifically, in the branch that currently does "if
(error) { return NextResponse.json([], { status: 200 }); }" and in the catch
block that returns 200, change them to return an appropriate server error
response (e.g., NextResponse.json({ error: 'Internal Server Error', details:
String(error) }, { status: 500 })) so the API surfaces failures instead of
pretending there are no appointments; preserve usage of NextResponse.json and
include the caught/received error variable for logging/response.

}
}
34 changes: 34 additions & 0 deletions app/api/admin/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function POST(req: NextRequest) {
try {
const { email, password } = await req.json();

const { data, error } = await supabase
.from("system_admins")
.select("*")
.eq("email", email)
.eq("password", password)
.single();
Comment thread
DHIRAVIYASUNDARAM marked this conversation as resolved.

if (error || !data) {
return NextResponse.json(
{ error: "Invalid credentials" },
{ status: 401 }
);
}

return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: "Login failed" },
{ status: 500 }
);
}
}
71 changes: 68 additions & 3 deletions app/api/appointments/book/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

export async function POST(_req: NextRequest) {
return NextResponse.json({ error: "Not implemented" }, { status: 501 });
}
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function POST(req: NextRequest) {
Comment thread
DHIRAVIYASUNDARAM marked this conversation as resolved.
try {
const { slotId, doctorId, patientId } = await req.json();

if (!slotId || !doctorId || !patientId) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}

const { data: slot } = await supabase
.from("slots")
.select("*")
.eq("id", slotId)
.single();
Comment thread
DHIRAVIYASUNDARAM marked this conversation as resolved.
Comment on lines +20 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Derive the appointment doctor from the slot, not from the request.

The handler never checks that doctorId matches slot.doctor_id, then uses the client value for both the duplicate check and the inserted appointment. A caller can therefore book doctor A's slot while storing doctor B on the appointment, which breaks dashboard joins and bypasses the "already have an active appointment with this doctor" rule for the real slot owner.

🩹 Suggested fix
-    const { slotId, doctorId, patientId } = await req.json();
+    const { slotId, patientId } = await req.json();
@@
-    if (!slotId || !doctorId || !patientId) {
+    if (!slotId || !patientId) {
       return NextResponse.json(
         { error: "Missing required fields" },
         { status: 400 }
       );
     }
@@
-    const { data: slot } = await supabase
+    const { data: slot } = await supabase
       .from("slots")
-      .select("*")
+      .select("id, doctor_id, is_booked")
       .eq("id", slotId)
       .single();
@@
+    const doctorId = slot.doctor_id;
+
     const { data: existing } = await supabase
       .from("appointments")
       .select("id")
       .eq("patient_id", patientId)
       .eq("doctor_id", doctorId)
       .eq("status", "active");

Also applies to: 33-54, 58-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/appointments/book/route.ts` around lines 20 - 24, The handler
currently trusts the incoming doctorId instead of using the slot owner; fetch
the slot (the existing `slot` result from the `slots` query) and derive the
appointment's doctor as `const doctorId = slot.doctor_id` (after checking `slot`
exists), then use that derived `doctorId` everywhere: in the duplicate-check
query that looks for an active appointment with the same doctor and patient, and
in the `insert` payload for the new appointment; ignore or remove the doctorId
coming from the request body so callers cannot substitute a different doctor.
Ensure the same change is applied to the other checks/inserts in the function
(the duplicate-check block and the appointment insert blocks referenced around
lines 33-54 and 58-61).


if (!slot || slot.is_booked) {
return NextResponse.json(
{ error: "Slot already booked" },
{ status: 400 }
);
}

const { data: existing } = await supabase
.from("appointments")
.select("id")
.eq("patient_id", patientId)
.eq("doctor_id", doctorId)
.eq("status", "active");

if (existing && existing.length > 0) {
return NextResponse.json(
{ error: "You already have an active appointment with this doctor" },
{ status: 400 }
);
}

const { error: insertError } = await supabase
.from("appointments")
.insert({
patient_id: patientId,
doctor_id: doctorId,
slot_id: slotId,
status: "active",
});

if (insertError) throw insertError;

await supabase
.from("slots")
.update({ is_booked: true })
.eq("id", slotId);

return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: "Booking failed" },
{ status: 500 }
);
}
}
Loading