Implement required changes#6
Implement required changes#6Vinothinithanancheziyan wants to merge 2 commits intobotcode-com:mainfrom
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
WalkthroughIntroduces admin functionality including login and dashboard pages, new API endpoints for admin operations and doctor dashboards, implements appointment booking and cancellation with authorization and validation logic, adds Supabase admin client, and establishes test infrastructure with unit and integration tests. Changes
Sequence DiagramssequenceDiagram
actor Admin
participant LoginPage as Admin Login Page
participant API as /api/admin/login
participant Supabase as Supabase (system_admins)
participant Browser as Browser Storage
Admin->>LoginPage: Enter email & password
LoginPage->>API: POST credentials as JSON
API->>Supabase: Query system_admins table
alt Credentials valid
Supabase-->>API: Admin record found
API-->>LoginPage: 200 OK + admin email
LoginPage->>Browser: Save admin_session to localStorage
LoginPage->>LoginPage: Navigate to /admin/dashboard
else Credentials invalid
Supabase-->>API: No matching record
API-->>LoginPage: 401 Unauthorized
LoginPage->>LoginPage: Display error message
end
sequenceDiagram
actor Patient
participant DashboardPage as Patient Dashboard Page
participant BookAPI as /api/appointments/book
participant Supabase as Supabase DB
participant Auth as Supabase Auth
Patient->>DashboardPage: Load page with Bearer token
DashboardPage->>BookAPI: POST {slotId, doctorId} + auth header
BookAPI->>Auth: Validate Bearer token → get patientId
alt Auth valid
BookAPI->>Supabase: validateBooking(slot, patient, doctor)
Supabase-->>BookAPI: Validation result
alt Validation passes
BookAPI->>Supabase: Update slot to is_booked=true
BookAPI->>Supabase: Insert new appointment (status=active)
Supabase-->>BookAPI: 200 OK + appointment details
BookAPI-->>DashboardPage: Success response
DashboardPage->>DashboardPage: Refresh appointments list
else Validation fails
BookAPI-->>DashboardPage: 400 Bad Request
DashboardPage->>DashboardPage: Display validation error
end
else Auth invalid
BookAPI-->>DashboardPage: 401 Unauthorized
DashboardPage->>DashboardPage: Clear session & redirect to login
end
sequenceDiagram
actor User as Patient/Doctor
participant DashboardPage as Dashboard Page
participant CancelAPI as /api/appointments/cancel
participant Auth as Supabase Auth
participant Supabase as Supabase DB
User->>DashboardPage: Click cancel appointment button
DashboardPage->>CancelAPI: POST {appointmentId, action} + Bearer token
CancelAPI->>Auth: Validate Bearer token → get userId
alt Auth valid
CancelAPI->>Supabase: Fetch appointment details
alt Appointment found
alt action = "done" (Doctor only)
alt userId matches doctor_id
CancelAPI->>Supabase: Update appointment status=done
Supabase-->>CancelAPI: 200 OK
else Doctor mismatch
CancelAPI-->>DashboardPage: 403 Forbidden
end
else action = "cancel"
CancelAPI->>Supabase: Check canCancelAppointment(patient rules)
alt Cancellation allowed
CancelAPI->>Supabase: Update appointment status=cancelled
CancelAPI->>Supabase: Update related slot is_booked=false
Supabase-->>CancelAPI: 200 OK
CancelAPI-->>DashboardPage: Success
else Cannot cancel (< 1 hour for patients)
CancelAPI-->>DashboardPage: 400 Bad Request
DashboardPage->>DashboardPage: Show error tooltip
end
end
else Appointment not found
CancelAPI-->>DashboardPage: 404 Not Found
end
else Auth invalid
CancelAPI-->>DashboardPage: 401 Unauthorized
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 36 minutes and 10 seconds.Comment |
There was a problem hiding this comment.
Code Review
This pull request implements an admin dashboard and login system, refactors appointment booking and cancellation logic into a shared utility, and introduces a testing suite with Vitest. Critical security vulnerabilities were found, specifically the use of plain text passwords for admin authentication and the absence of session verification on admin-only API routes. Additionally, the booking logic relies on a manual rollback mechanism that should be replaced with database transactions to ensure atomicity and prevent potential race conditions.
| .eq("email", email) | ||
| .eq("password", password) |
|
|
||
| export async function GET(_req: NextRequest) { | ||
| try { | ||
| // In a real app, we would verify the admin session/token here. |
| // Rollback slot update | ||
| await supabaseAdmin.from("slots").update({ is_booked: false }).eq("id", slotId); |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/admin/login/page.tsx`:
- Around line 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.
In `@app/api/appointments/book/route.ts`:
- Around line 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.
In `@app/api/appointments/cancel/route.ts`:
- Around line 31-35: When handling action === "done" in the cancel route, add
validation that appt.status is "active" before allowing the doctor to mark it
done; if appt.status is not "active" return a NextResponse.json error (e.g.,
400) indicating the appointment cannot be marked done from its current status.
Update the branch that currently checks only appt.doctor_id (referencing action,
appt, user, and NextResponse.json) to first verify appt.status === "active" and
only then allow the transition.
- Line 8: Validate the incoming action value before using it: check the
extracted variable action (from const { appointmentId, action } = await
req.json()) against an allowlist ["cancel", "done"] and short-circuit with a 4xx
error (or throw) if it's not one of those values; then use the validated action
to derive the appointment status instead of falling through to a default "done"
value. Ensure this validation occurs immediately after parsing the request and
before any logic that sets status or updates the appointment.
In `@app/api/doctor/dashboard/route.ts`:
- Around line 14-19: The code starts slot/appointment reads in Promise.all
before confirming the caller is a doctor, causing incorrect 500s for valid users
without a doctors row; change the first query to use maybeSingle() on
supabaseAdmin.from("doctors").select("*").eq("id", user.id).maybeSingle(), await
it first (resolve doctorRes), check if doctorRes.error or doctorRes.data is null
and return the appropriate 403/404 response, and only then run the fan-out
queries for slots/appointments (currently assigned to slotData and apptData) —
remove them from the initial Promise.all and run them after the doctor
authorization passes.
- Around line 20-24: The slots query currently pulls every appointment for each
slot and the UI reads appointments?.[0]?.patients?.name, causing
historical/cancelled patients to leak; fix by filtering the nested appointments
to only include the active booking (e.g., status = 'booked') and limit to a
single appointment per slot. Update the chain that calls
supabaseAdmin.from("slots").select("*, appointments(patients(name))") to instead
join/filter appointments (for example use appointments!inner or add
.eq("appointments.status","booked") plus ordering on appointments (e.g., by
start_time desc) and .limit(1, { foreignTable: 'appointments' }) so the returned
slot.appointments contains only the current booked appointment used by
app/doctor/dashboard/page.tsx when reading
slot.appointments?.[0]?.patients?.name.
In `@lib/appointment-logic.ts`:
- Around line 9-17: The slot lookup currently only selects is_booked and doesn't
verify ownership; update the query in lib/appointment-logic.ts that fetches the
slot (the const { data: slot, error: slotError } = await
supabase.from("slots").select(...).eq("id", slotId).single()) to also select
doctor_id, then after the existing existence/is_booked checks validate that
slot.doctor_id === doctorId (the doctorId passed into the appointment creation
flow) and return a failing result (e.g., { valid: false, error: "Slot does not
belong to doctor" }) if they do not match.
- Around line 43-58: Add a status guard for doctor cancellations: currently when
isPatient is false the code falls through to return { canCancel: true } even if
appointment.status is "cancelled" or "done"; update the non-patient branch to
check appointment.status (same check used in the isPatient branch) and return {
canCancel: false, error: "Appointment is already done or cancelled" } when
status !== "active", otherwise return { canCancel: true } so both paths use the
appointment.status, appointment.slots.start_time and canCancel semantics
consistently.
In `@lib/supabase-admin.ts`:
- Around line 3-7: The module currently uses non-null assertions for supabaseUrl
and supabaseServiceKey which can hide missing env vars; update the top-level
initialization to validate process.env.NEXT_PUBLIC_SUPABASE_URL and
process.env.SUPABASE_SERVICE_ROLE_KEY and throw a clear error if either is
missing so the process fails fast; then call createClient with the validated
values to produce supabaseAdmin (refer to supabaseUrl, supabaseServiceKey,
supabaseAdmin, and createClient in the file).
In `@seed.mjs`:
- Around line 25-71: The seed script still inserts fixture data for "slots" and
the pre-seeded appointment with plain inserts (causing duplicates on re-run);
change those insert calls to idempotent upserts (or insert(...).onConflict(...))
so re-running the seed reuses the same records—use
supabase.from("slots").upsert(...) for the slots array (or provide deterministic
ids or an onConflict key such as doctor_id+starts_at) and likewise replace the
plain insert for the pre-seeded appointment with
supabase.from("appointments").upsert(...) (or insert with onConflict) and keep
the same error logging pattern as in the doctors/patients upserts.
In `@tests/integration/workflows.test.ts`:
- Around line 35-65: The test mutates shared state but only cleans up on the
happy path—wrap the booking actions and assertions in a try/finally so cleanup
always runs; specifically, in the it("should complete the booking flow
successfully") test use a try { ... } finally { await
supabaseAdmin.from("appointments").delete().eq("id", appt?.id); await
supabaseAdmin.from("slots").update({ is_booked: false }).eq("id", testSlotId); }
pattern (ensure appt is declared in an outer scope so appt?.id is usable), and
apply the same try/finally protection to the other test referenced (lines 68-93)
that also mutates slots/appointments; keep using supabaseAdmin, testSlotId,
testPatientId, testDoctorId and ensure errors are re-thrown or allowed to
surface after cleanup.
- Around line 12-16: The current fixtures fetch doctors, patients, and slots
independently which can yield a slot whose slots.doctor_id doesn't match the
testDoctorId used later (testDoctorId/testSlotId), so change the fetch to first
obtain the doctor (docRes / testDoctorId) and then query slots with a filter on
doctor_id = that doctor id and is_booked = false (instead of the independent
supabaseAdmin.from("slots") call); update any places using Promise.all to fetch
the doctor first (maybeSingle on "doctors"), then use its id when calling
supabaseAdmin.from("slots").select(...).eq("doctor_id", docId).eq("is_booked",
false).limit(1).maybeSingle() so testSlotId always belongs to testDoctorId.
In `@tests/setup.ts`:
- Around line 4-7: The current self-assignments to process.env
(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY) are no-ops and do not provide or validate test
credentials; update tests/setup.ts to either (a) load concrete test values from
a .env.test or a test fixture into these environment variables when absent, or
(b) perform an explicit sanity check and throw an error if any of
process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
or process.env.SUPABASE_SERVICE_ROLE_KEY are undefined so tests fail fast;
locate the initialization code that touches those three env vars and implement
one of those two fixes (load defaults for CI/local tests or validate and throw)
accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9d13aedb-65a7-4ec3-b00d-11688bda8485
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
app/admin/dashboard/page.tsxapp/admin/login/page.tsxapp/api/admin/appointments/route.tsapp/api/admin/login/route.tsapp/api/appointments/book/route.tsapp/api/appointments/cancel/route.tsapp/api/doctor/dashboard/route.tsapp/doctor/dashboard/page.tsxapp/patient/dashboard/page.tsxlib/appointment-logic.tslib/supabase-admin.tsnext-env.d.tspackage.jsonseed.mjstests/integration/workflows.test.tstests/setup.tstests/unit/appointments.test.tsvitest.config.ts
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| // Use extracted logic | ||
| const validation = await validateBooking(supabaseAdmin, patientId, doctorId, slotId); | ||
| if (!validation.valid) { | ||
| return NextResponse.json({ error: validation.error }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
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.
| return NextResponse.json({ error: "Not implemented" }, { status: 501 }); | ||
| export async function POST(req: NextRequest) { | ||
| try { | ||
| const { appointmentId, action } = await req.json(); // action: "cancel" | "done" |
There was a problem hiding this comment.
Validate action against an allowlist before processing.
If action is neither "cancel" nor "done", the code falls through the conditionals (lines 26-35) without any validation, then defaults to "done" status on line 38. This could allow unexpected status updates.
🛡️ Proposed fix to validate action
const { appointmentId, action } = await req.json(); // action: "cancel" | "done"
+
+ if (action !== "cancel" && action !== "done") {
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 });
+ }
const authHeader = req.headers.get("Authorization");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/appointments/cancel/route.ts` at line 8, Validate the incoming action
value before using it: check the extracted variable action (from const {
appointmentId, action } = await req.json()) against an allowlist ["cancel",
"done"] and short-circuit with a 4xx error (or throw) if it's not one of those
values; then use the validated action to derive the appointment status instead
of falling through to a default "done" value. Ensure this validation occurs
immediately after parsing the request and before any logic that sets status or
updates the appointment.
| } else if (action === "done") { | ||
| if (appt.doctor_id !== user.id) { | ||
| return NextResponse.json({ error: "Only doctors can mark as done" }, { status: 403 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Add status validation for the "done" action.
The "done" action only verifies the user is the doctor but doesn't check if the appointment is still "active". This allows marking cancelled or already-done appointments as "done", leading to incorrect state transitions.
🔧 Proposed fix
} else if (action === "done") {
if (appt.doctor_id !== user.id) {
return NextResponse.json({ error: "Only doctors can mark as done" }, { status: 403 });
}
+ if (appt.status !== "active") {
+ return NextResponse.json({ error: "Appointment is not active" }, { status: 400 });
+ }
}📝 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.
| } else if (action === "done") { | |
| if (appt.doctor_id !== user.id) { | |
| return NextResponse.json({ error: "Only doctors can mark as done" }, { status: 403 }); | |
| } | |
| } | |
| } else if (action === "done") { | |
| if (appt.doctor_id !== user.id) { | |
| return NextResponse.json({ error: "Only doctors can mark as done" }, { status: 403 }); | |
| } | |
| if (appt.status !== "active") { | |
| return NextResponse.json({ error: "Appointment is not active" }, { status: 400 }); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/appointments/cancel/route.ts` around lines 31 - 35, When handling
action === "done" in the cancel route, add validation that appt.status is
"active" before allowing the doctor to mark it done; if appt.status is not
"active" return a NextResponse.json error (e.g., 400) indicating the appointment
cannot be marked done from its current status. Update the branch that currently
checks only appt.doctor_id (referencing action, appt, user, and
NextResponse.json) to first verify appt.status === "active" and only then allow
the transition.
| const [doctorRes, slotData, apptData] = await Promise.all([ | ||
| supabaseAdmin | ||
| .from("doctors") | ||
| .select("*") | ||
| .eq("id", user.id) | ||
| .single(), |
There was a problem hiding this comment.
Authorize the caller as a doctor before fan-outing service-role queries.
A valid Supabase user without a matching doctors row currently falls into the generic 500 path, and the Promise.all starts the slot/appointment reads before that check is resolved. This route should first load the doctor record with maybeSingle() and return 403/404 when the caller is not a doctor.
Proposed fix
- const [doctorRes, slotData, apptData] = await Promise.all([
- supabaseAdmin
- .from("doctors")
- .select("*")
- .eq("id", user.id)
- .single(),
+ const { data: doctor, error: doctorError } = await supabaseAdmin
+ .from("doctors")
+ .select("*")
+ .eq("id", user.id)
+ .maybeSingle();
+
+ if (doctorError) {
+ return NextResponse.json({ error: "Failed to fetch doctor" }, { status: 500 });
+ }
+
+ if (!doctor) {
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+ }
+
+ const [slotData, apptData] = await Promise.all([
supabaseAdmin
.from("slots")
.select("*, appointments(patients(name))")
.eq("doctor_id", user.id)
.order("start_time"),
supabaseAdmin
.from("appointments")
.select("id, status, created_at, patients(name), slots(start_time, end_time)")
.eq("doctor_id", user.id)
.order("created_at", { ascending: false })
]);
- if (doctorRes.error || slotData.error || apptData.error) {
+ if (slotData.error || apptData.error) {
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 });
}
return NextResponse.json({
- doctor: doctorRes.data,
+ doctor,
slots: slotData.data,
appointments: apptData.data
});Also applies to: 32-34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/doctor/dashboard/route.ts` around lines 14 - 19, The code starts
slot/appointment reads in Promise.all before confirming the caller is a doctor,
causing incorrect 500s for valid users without a doctors row; change the first
query to use maybeSingle() on supabaseAdmin.from("doctors").select("*").eq("id",
user.id).maybeSingle(), await it first (resolve doctorRes), check if
doctorRes.error or doctorRes.data is null and return the appropriate 403/404
response, and only then run the fan-out queries for slots/appointments
(currently assigned to slotData and apptData) — remove them from the initial
Promise.all and run them after the doctor authorization passes.
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; | ||
| const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; | ||
|
|
||
| // Use this client for operations that need to bypass RLS (e.g., seeding, admin dashboard) | ||
| export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey); |
There was a problem hiding this comment.
Fail fast when required Supabase env vars are missing.
The non-null assertions only silence TypeScript. If either variable is absent, this module still initializes with undefined and every admin route/test fails later with a much less actionable error.
🧯 Proposed fix
import { createClient } from "@supabase/supabase-js";
-const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
-const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
+function requireEnv(name: string) {
+ const value = process.env[name];
+ if (!value) throw new Error(`Missing required environment variable: ${name}`);
+ return value;
+}
+
+const supabaseUrl = requireEnv("NEXT_PUBLIC_SUPABASE_URL");
+const supabaseServiceKey = requireEnv("SUPABASE_SERVICE_ROLE_KEY");
// Use this client for operations that need to bypass RLS (e.g., seeding, admin dashboard)
export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey);📝 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.
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; | |
| const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; | |
| // Use this client for operations that need to bypass RLS (e.g., seeding, admin dashboard) | |
| export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey); | |
| import { createClient } from "@supabase/supabase-js"; | |
| function requireEnv(name: string) { | |
| const value = process.env[name]; | |
| if (!value) throw new Error(`Missing required environment variable: ${name}`); | |
| return value; | |
| } | |
| const supabaseUrl = requireEnv("NEXT_PUBLIC_SUPABASE_URL"); | |
| const supabaseServiceKey = requireEnv("SUPABASE_SERVICE_ROLE_KEY"); | |
| // Use this client for operations that need to bypass RLS (e.g., seeding, admin dashboard) | |
| export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/supabase-admin.ts` around lines 3 - 7, The module currently uses non-null
assertions for supabaseUrl and supabaseServiceKey which can hide missing env
vars; update the top-level initialization to validate
process.env.NEXT_PUBLIC_SUPABASE_URL and process.env.SUPABASE_SERVICE_ROLE_KEY
and throw a clear error if either is missing so the process fails fast; then
call createClient with the validated values to produce supabaseAdmin (refer to
supabaseUrl, supabaseServiceKey, supabaseAdmin, and createClient in the file).
| for (const doctor of doctors) { | ||
| let user = users.find(u => u.email === doctor.email); | ||
|
|
||
| if (!user) { | ||
| const { data, error } = await supabase.auth.admin.createUser({ | ||
| email: doctor.email, | ||
| password: doctor.password, | ||
| email_confirm: true, | ||
| }); | ||
| if (error) { console.error("create user", doctor.email, error.message); continue; } | ||
| user = data.user; | ||
| } | ||
|
|
||
| doctorIds[doctor.email] = user.id; | ||
|
|
||
| const { error: e } = await supabase.from("doctors").upsert({ | ||
| id: user.id, | ||
| name: doctor.name, | ||
| email: doctor.email, | ||
| specialty: doctor.specialty, | ||
| }); | ||
| if (e) console.error("insert doctor", doctor.email, e.message); | ||
| else console.log("created", doctor.email); | ||
| if (e) console.error("upsert doctor", doctor.email, e.message); | ||
| else console.log("synced", doctor.email); | ||
| } | ||
|
|
||
| for (const patient of patients) { | ||
| const { data, error } = await supabase.auth.admin.createUser({ | ||
| email: patient.email, | ||
| password: patient.password, | ||
| email_confirm: true, | ||
| }); | ||
| if (error) { console.error("create user", patient.email, error.message); continue; } | ||
|
|
||
| patientIds[patient.email] = data.user.id; | ||
|
|
||
| const { error: e } = await supabase.from("patients").insert({ | ||
| id: data.user.id, | ||
| let user = users.find(u => u.email === patient.email); | ||
|
|
||
| if (!user) { | ||
| const { data, error } = await supabase.auth.admin.createUser({ | ||
| email: patient.email, | ||
| password: patient.password, | ||
| email_confirm: true, | ||
| }); | ||
| if (error) { console.error("create user", patient.email, error.message); continue; } | ||
| user = data.user; | ||
| } | ||
|
|
||
| patientIds[patient.email] = user.id; | ||
|
|
||
| const { error: e } = await supabase.from("patients").upsert({ | ||
| id: user.id, | ||
| name: patient.name, | ||
| email: patient.email, | ||
| }); | ||
| if (e) console.error("insert patient", patient.email, e.message); | ||
| else console.log("created", patient.email); | ||
| if (e) console.error("upsert patient", patient.email, e.message); | ||
| else console.log("synced", patient.email); |
There was a problem hiding this comment.
The seed is still only partially idempotent.
This change correctly reuses auth users and upserts doctors/patients, but the script still does plain inserts for slots and the pre-seeded appointment later in the file. Re-running npm run seed will keep duplicating fixture data and skew both integration tests and manual verification.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@seed.mjs` around lines 25 - 71, The seed script still inserts fixture data
for "slots" and the pre-seeded appointment with plain inserts (causing
duplicates on re-run); change those insert calls to idempotent upserts (or
insert(...).onConflict(...)) so re-running the seed reuses the same records—use
supabase.from("slots").upsert(...) for the slots array (or provide deterministic
ids or an onConflict key such as doctor_id+starts_at) and likewise replace the
plain insert for the pre-seeded appointment with
supabase.from("appointments").upsert(...) (or insert with onConflict) and keep
the same error logging pattern as in the doctors/patients upserts.
| const [docRes, patRes, slotRes] = await Promise.all([ | ||
| supabaseAdmin.from("doctors").select("id").limit(1).maybeSingle(), | ||
| supabaseAdmin.from("patients").select("id").limit(1).maybeSingle(), | ||
| supabaseAdmin.from("slots").select("id").eq("is_booked", false).limit(1).maybeSingle() | ||
| ]); |
There was a problem hiding this comment.
The selected slot may belong to a different doctor than testDoctorId.
doctors, patients, and slots are fetched independently here, but the tests later create appointments using both doctor_id: testDoctorId and slot_id: testSlotId. That can produce impossible fixtures where the appointment’s doctor does not match slots.doctor_id, so the test can pass without exercising a realistic booking flow.
Proposed fix
- const [docRes, patRes, slotRes] = await Promise.all([
- supabaseAdmin.from("doctors").select("id").limit(1).maybeSingle(),
- supabaseAdmin.from("patients").select("id").limit(1).maybeSingle(),
- supabaseAdmin.from("slots").select("id").eq("is_booked", false).limit(1).maybeSingle()
- ]);
+ const [patRes, slotRes] = await Promise.all([
+ supabaseAdmin.from("patients").select("id").limit(1).maybeSingle(),
+ supabaseAdmin
+ .from("slots")
+ .select("id, doctor_id")
+ .eq("is_booked", false)
+ .limit(1)
+ .maybeSingle()
+ ]);
...
- const { data: doctor, error: docError } = docRes;
const { data: patient, error: patError } = patRes;
const { data: slot, error: slotError } = slotRes;
...
- if (docError || patError || slotError) {
- throw new Error(`Setup failed: ${docError?.message || patError?.message || slotError?.message}`);
+ if (patError || slotError) {
+ throw new Error(`Setup failed: ${patError?.message || slotError?.message}`);
}
...
- if (!doctor || !patient || !slot) {
+ if (!patient || !slot) {
throw new Error("Setup failed: Missing test data in database. Please run npm run seed first.");
}
- testDoctorId = doctor.id;
+ testDoctorId = slot.doctor_id;
testPatientId = patient.id;
testSlotId = slot.id;📝 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.
| const [docRes, patRes, slotRes] = await Promise.all([ | |
| supabaseAdmin.from("doctors").select("id").limit(1).maybeSingle(), | |
| supabaseAdmin.from("patients").select("id").limit(1).maybeSingle(), | |
| supabaseAdmin.from("slots").select("id").eq("is_booked", false).limit(1).maybeSingle() | |
| ]); | |
| const [patRes, slotRes] = await Promise.all([ | |
| supabaseAdmin.from("patients").select("id").limit(1).maybeSingle(), | |
| supabaseAdmin | |
| .from("slots") | |
| .select("id, doctor_id") | |
| .eq("is_booked", false) | |
| .limit(1) | |
| .maybeSingle() | |
| ]); | |
| const { data: patient, error: patError } = patRes; | |
| const { data: slot, error: slotError } = slotRes; | |
| if (patError || slotError) { | |
| throw new Error(`Setup failed: ${patError?.message || slotError?.message}`); | |
| } | |
| if (!patient || !slot) { | |
| throw new Error("Setup failed: Missing test data in database. Please run npm run seed first."); | |
| } | |
| testDoctorId = slot.doctor_id; | |
| testPatientId = patient.id; | |
| testSlotId = slot.id; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/integration/workflows.test.ts` around lines 12 - 16, The current
fixtures fetch doctors, patients, and slots independently which can yield a slot
whose slots.doctor_id doesn't match the testDoctorId used later
(testDoctorId/testSlotId), so change the fetch to first obtain the doctor
(docRes / testDoctorId) and then query slots with a filter on doctor_id = that
doctor id and is_booked = false (instead of the independent
supabaseAdmin.from("slots") call); update any places using Promise.all to fetch
the doctor first (maybeSingle on "doctors"), then use its id when calling
supabaseAdmin.from("slots").select(...).eq("doctor_id", docId).eq("is_booked",
false).limit(1).maybeSingle() so testSlotId always belongs to testDoctorId.
| it("should complete the booking flow successfully", async () => { | ||
| // 1. Check initial state | ||
| const { data: initialSlot } = await supabaseAdmin.from("slots").select("is_booked").eq("id", testSlotId).single(); | ||
| expect(initialSlot).not.toBeNull(); | ||
| expect(initialSlot!.is_booked).toBe(false); | ||
|
|
||
| // 2. Perform booking | ||
| const { data: appt, error: apptError } = await supabaseAdmin.from("appointments").insert({ | ||
| patient_id: testPatientId, | ||
| doctor_id: testDoctorId, | ||
| slot_id: testSlotId, | ||
| status: "active", | ||
| }).select().single(); | ||
|
|
||
| if (apptError) throw apptError; | ||
|
|
||
| expect(appt).not.toBeNull(); | ||
| expect(appt!.status).toBe("active"); | ||
|
|
||
| await supabaseAdmin.from("slots").update({ is_booked: true }).eq("id", testSlotId); | ||
|
|
||
| expect(appt!.status).toBe("active"); | ||
|
|
||
| // 3. Verify final state | ||
| const { data: finalSlot } = await supabaseAdmin.from("slots").select("is_booked").eq("id", testSlotId).single(); | ||
| expect(finalSlot).not.toBeNull(); | ||
| expect(finalSlot!.is_booked).toBe(true); | ||
|
|
||
| // Clean up | ||
| await supabaseAdmin.from("appointments").delete().eq("id", appt.id); | ||
| await supabaseAdmin.from("slots").update({ is_booked: false }).eq("id", testSlotId); |
There was a problem hiding this comment.
Protect database cleanup with finally.
Both tests mutate a shared live Supabase instance, but cleanup only runs on the happy path. If any insert, update, or assertion fails, you can leave behind appointments rows or a stuck slots.is_booked = true, which will poison later tests and reruns.
Also applies to: 68-93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/integration/workflows.test.ts` around lines 35 - 65, The test mutates
shared state but only cleans up on the happy path—wrap the booking actions and
assertions in a try/finally so cleanup always runs; specifically, in the
it("should complete the booking flow successfully") test use a try { ... }
finally { await supabaseAdmin.from("appointments").delete().eq("id", appt?.id);
await supabaseAdmin.from("slots").update({ is_booked: false }).eq("id",
testSlotId); } pattern (ensure appt is declared in an outer scope so appt?.id is
usable), and apply the same try/finally protection to the other test referenced
(lines 68-93) that also mutates slots/appointments; keep using supabaseAdmin,
testSlotId, testPatientId, testDoctorId and ensure errors are re-thrown or
allowed to surface after cleanup.
| // Mock environment variables if needed | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; | ||
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; | ||
| process.env.SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; |
There was a problem hiding this comment.
This setup does not actually mock or validate the test env.
The self-assignments are no-ops, so missing Supabase credentials are still discovered only after a later network call fails. Fail fast here, or load concrete test values.
🛠️ Proposed fix
import "@testing-library/jest-dom";
import { vi } from "vitest";
-// Mock environment variables if needed
-process.env.NEXT_PUBLIC_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
-process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
-process.env.SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
+const requiredEnv = [
+ "NEXT_PUBLIC_SUPABASE_URL",
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY",
+ "SUPABASE_SERVICE_ROLE_KEY",
+] as const;
+
+for (const key of requiredEnv) {
+ if (!process.env[key]) {
+ throw new Error(`Missing required test env: ${key}`);
+ }
+}📝 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.
| // Mock environment variables if needed | |
| process.env.NEXT_PUBLIC_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; | |
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; | |
| process.env.SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; | |
| import "@testing-library/jest-dom"; | |
| import { vi } from "vitest"; | |
| const requiredEnv = [ | |
| "NEXT_PUBLIC_SUPABASE_URL", | |
| "NEXT_PUBLIC_SUPABASE_ANON_KEY", | |
| "SUPABASE_SERVICE_ROLE_KEY", | |
| ] as const; | |
| for (const key of requiredEnv) { | |
| if (!process.env[key]) { | |
| throw new Error(`Missing required test env: ${key}`); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/setup.ts` around lines 4 - 7, The current self-assignments to
process.env (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY) are no-ops and do not provide or validate test
credentials; update tests/setup.ts to either (a) load concrete test values from
a .env.test or a test fixture into these environment variables when absent, or
(b) perform an explicit sanity check and throw an error if any of
process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
or process.env.SUPABASE_SERVICE_ROLE_KEY are undefined so tests fail fast;
locate the initialization code that touches those three env vars and implement
one of those two fixes (load defaults for CI/local tests or validate and throw)
accordingly.
Viewed appointment-logic.ts:1-60
Viewed workflows.test.ts:1-95
Viewed route.ts:1-31
Viewed route.ts:1-27
Listed directory appointments
Implement recruitment task changes
I have completed the backend and administrative functionality for the appointment booking system. This implementation ensures a reliable and secure workflow for patients, doctors, and system administrators.
Features and Fixes
validateBookinginlib/appointment-logic.tsto enforce business rules:canCancelAppointmentwhich allows patients to cancel only if the appointment is more than 1 hour away, while allowing doctors/admins to cancel at any time.POST /api/appointments/book: Handles new appointment creation and slot updates.POST /api/appointments/cancel: Handles appointment cancellation and slot release.POST /api/admin/login: Basic administrative authentication.GET /api/admin/appointments: Secure endpoint for administrators to view all system appointments with related patient, doctor, and slot details.tests/unit/appointments.test.ts.tests/integration/workflows.test.tsto verify end-to-end booking and cancellation flows against a live Supabase instance.Assumptions
schema.sqland that the environment variables are correctly set in.env.supabaseAdmin(Service Role Key) is used for backend operations to ensure data integrity and bypass RLS where necessary.How to Test
.envfile has valid Supabase credentials.npm test/api/admin/loginwith admin credentials./api/appointments/bookwith a validpatient_id,doctor_id, andslot_id./api/appointments/cancelwith anappointment_id./api/admin/appointmentsto see the updated status of all bookings.Changes Summary
lib/appointment-logic.tsapp/api/appointments/*app/api/admin/*tests/unit/*,tests/integration/*Summary by CodeRabbit
New Features
Tests
Chores