Sitelet https://github.com/botcode-com/task/pull/6/commits/13581850c770b6e02e88e3cabc35907bbdd62445
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Implement required changes
  • Loading branch information
Vino1705
Vino1705 committed Apr 29, 2026
commit 13581850c770b6e02e88e3cabc35907bbdd62445
140 changes: 140 additions & 0 deletions app/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";

type Appointment = {
id: string;
status: "active" | "done" | "cancelled";
created_at: string;
patient: { name: string; email: string };
doctor: { name: string; specialty: string };
slot: { start_time: string; end_time: string };
};

function formatDateTime(iso: string) {
return new Date(iso).toLocaleString("en-IN", {
dateStyle: "medium",
timeStyle: "short",
});
}

export default function AdminDashboard() {
const router = useRouter();
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [loading, setLoading] = useState(true);
const [admin, setAdmin] = useState<{ email: string } | null>(null);

useEffect(() => {
const session = localStorage.getItem("admin_session");
if (!session) {
router.push("/admin/login");
return;
}
setAdmin(JSON.parse(session));

async function fetchAppointments() {
const res = await fetch("/api/admin/appointments");
const data = await res.json();
if (res.ok) {
setAppointments(data.appointments);
}
setLoading(false);
}

fetchAppointments();
}, [router]);

function handleLogout() {
localStorage.removeItem("admin_session");
router.push("/admin/login");
}

if (loading) {
return (
<main className="flex min-h-screen items-center justify-center">
<p className="text-gray-500">Loading appointments...</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-2xl font-bold">Admin Dashboard</h1>
<p className="text-sm text-gray-500">Logged in as {admin?.email}</p>
</div>
<button
onClick={handleLogout}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100"
>
Logout
</button>
</div>

<section className="rounded-xl border bg-white shadow-sm overflow-hidden">
<div className="border-b bg-gray-50 px-6 py-4">
<h2 className="text-lg font-semibold text-gray-800">All Appointments</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-gray-50 text-gray-600 uppercase text-xs font-bold">
<tr>
<th className="px-6 py-3">Patient</th>
<th className="px-6 py-3">Doctor</th>
<th className="px-6 py-3">Slot Time</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Created At</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{appointments.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-10 text-center text-gray-500">
No appointments found in the system.
</td>
</tr>
) : (
appointments.map((appt) => (
<tr key={appt.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{appt.patient?.name}</div>
<div className="text-xs text-gray-500">{appt.patient?.email}</div>
</td>
<td className="px-6 py-4">
<div className="font-medium text-gray-900">{appt.doctor?.name}</div>
<div className="text-xs text-gray-500">{appt.doctor?.specialty}</div>
</td>
<td className="px-6 py-4 text-gray-600">
{formatDateTime(appt.slot?.start_time)}
</td>
<td className="px-6 py-4">
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
appt.status === "active"
? "bg-blue-100 text-blue-700"
: appt.status === "done"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{appt.status.charAt(0).toUpperCase() + appt.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 text-gray-500">
{new Date(appt.created_at).toLocaleDateString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
</main>
);
}
84 changes: 84 additions & 0 deletions app/admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"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);

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) {
// Basic session handling for admin
localStorage.setItem("admin_session", JSON.stringify(data.admin));
router.push("/admin/dashboard");
} else {
setError(data.error || "Login failed");
setLoading(false);
}
Comment on lines +14 to +34

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

Reset loading on thrown fetch/JSON errors.

If the request fails at the network layer or returns invalid JSON, handleSubmit throws before setLoading(false) runs, leaving the form stuck in its disabled state with no feedback.

🛠️ Proposed fix
   async function handleSubmit(e: React.FormEvent) {
     e.preventDefault();
     setError("");
     setLoading(true);
-
-    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) {
-      // Basic session handling for admin
-      localStorage.setItem("admin_session", JSON.stringify(data.admin));
-      router.push("/admin/dashboard");
-    } else {
-      setError(data.error || "Login failed");
-      setLoading(false);
-    }
+    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();
+
+      if (res.ok) {
+        localStorage.setItem("admin_session", JSON.stringify(data.admin));
+        router.push("/admin/dashboard");
+        return;
+      }
+
+      setError(data.error || "Login failed");
+    } catch {
+      setError("Login failed. Please try again.");
+    } finally {
+      setLoading(false);
+    }
   }
🤖 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 - 34, handleSubmit can throw on
network/fetch/JSON errors and never resets loading; wrap the async fetch/await
and JSON parse in a try/catch and ensure setLoading(false) runs in a finally
block. Specifically, in the handleSubmit function surrounding the
fetch("/api/admin/login") and await res.json() calls, add try { ... } catch
(err) { setError(err?.message || "Network error"); } finally {
setLoading(false); } and keep the existing success path (localStorage.setItem
and router.push) inside the try so the UI always unblocks on errors or
completion.

}

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">
Email
</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-blue-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-blue-500"
/>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={loading}
className="rounded-lg bg-blue-600 py-2 font-medium text-white hover:bg-blue-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-blue-600 hover:underline">
← Back to home
</Link>
</p>
</div>
</main>
);
}
30 changes: 30 additions & 0 deletions app/api/admin/appointments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import { supabaseAdmin } from "@/lib/supabase-admin";

export async function GET(_req: NextRequest) {
try {
// In a real app, we would verify the admin session/token here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

The comment indicates that admin session verification is missing. This is a critical security flaw as any user can access this endpoint. You must implement authentication middleware or a check to ensure the requester is an administrator.


const { data: appointments, error } = await supabaseAdmin
.from("appointments")
.select(`
id,
status,
created_at,
patient:patients(name, email),
doctor:doctors(name, specialty),
slot:slots(start_time, end_time)
`)
.order("created_at", { ascending: false });

if (error) {
console.error("Admin fetch appointments error:", error);
return NextResponse.json({ error: "Failed to fetch appointments" }, { status: 500 });
}

return NextResponse.json({ appointments });
} catch (err) {
console.error("Admin appointments route error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
26 changes: 26 additions & 0 deletions app/api/admin/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { supabaseAdmin } from "@/lib/supabase-admin";

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

const { data: admin, error } = await supabaseAdmin
.from("system_admins")
.select("*")
.eq("email", email)
.eq("password", password)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

Storing and comparing passwords in plain text is a critical security vulnerability. Even for a recruitment task, you should use a hashing library like bcrypt to hash passwords before storing them and compare the hash during login.

.maybeSingle();

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

// In a real app, we would issue a JWT here.
// For this implementation, we return success and the client handles a basic session.
return NextResponse.json({ message: "Login successful", admin: { email: admin.email } });
} catch (err) {
console.error("Admin login error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
52 changes: 50 additions & 2 deletions app/api/appointments/book/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";
import { supabaseAdmin } from "@/lib/supabase-admin";
import { validateBooking } from "@/lib/appointment-logic";

export async function POST(_req: NextRequest) {
return NextResponse.json({ error: "Not implemented" }, { status: 501 });
export async function POST(req: NextRequest) {
try {
const { slotId, doctorId } = await req.json();

const authHeader = req.headers.get("Authorization");
if (!authHeader) return NextResponse.json({ error: "Missing authorization header" }, { status: 401 });

const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: userError } = await supabase.auth.getUser(token);
if (userError || !user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

const patientId = user.id;

// Use extracted logic
const validation = await validateBooking(supabaseAdmin, patientId, doctorId, slotId);
if (!validation.valid) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
Comment on lines +19 to +23

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

Reject past slots on the server as part of booking validation.

Right now the “future slots only” rule lives only in app/patient/dashboard/page.tsx (Line 71-Line 76 there). validateBooking(...) still accepts any unbooked slot, so a direct POST to this route can book a past appointment. Please extend the server-side validation to load start_time and fail when the slot has already started.

🤖 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 19 - 23, validateBooking
currently accepts any unbooked slot, allowing past slots to be booked via POST;
update the server-side validation in the validateBooking function (the same
function called from route.ts with supabaseAdmin, patientId, doctorId, slotId)
to load the slot's start_time from the DB and compare it to the current time,
and return validation.valid = false with an appropriate error when the
slot.start_time is <= now; ensure the query fetching the slot includes the
start_time field and that the route's existing call to validateBooking continues
to handle the returned { valid, error } shape.


// 4. Update slot and create appointment (Atomic update using admin client)
const { error: slotUpdateError } = await supabaseAdmin
.from("slots")
.update({ is_booked: true })
.eq("id", slotId);

if (slotUpdateError) {
return NextResponse.json({ error: "Failed to update slot" }, { status: 500 });
}

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

if (insertError) {
// Rollback slot update
await supabaseAdmin.from("slots").update({ is_booked: false }).eq("id", slotId);
Comment on lines +47 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This manual rollback approach is prone to race conditions and failure. Consider using a database transaction to ensure atomicity between updating the slot and creating the appointment.

return NextResponse.json({ error: "Failed to create appointment" }, { status: 500 });
}

return NextResponse.json({ message: "Appointment booked successfully" });
} catch (err) {
console.error("Booking error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
Loading