Sitelet https://github.com/botcode-com/task/pull/6/commits/af596b9f518a34d394e0ada615e3c914a3dd57aa
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
Prev Previous commit
Implemented
  • Loading branch information
Vino1705
Vino1705 committed Apr 29, 2026
commit af596b9f518a34d394e0ada615e3c914a3dd57aa
20 changes: 18 additions & 2 deletions app/api/admin/appointments/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { supabaseAdmin } from "@/lib/supabase-admin";
import jwt from "jsonwebtoken";

export async function GET(_req: NextRequest) {
const JWT_SECRET = process.env.JWT_SECRET || "default-secret";

export async function GET(req: NextRequest) {
try {
// In a real app, we would verify the admin session/token here.
const token = req.cookies.get("admin_session")?.value;

if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
const decoded = jwt.verify(token, JWT_SECRET) as { role: string };
if (decoded.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
} catch (err) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}

const { data: appointments, error } = await supabaseAdmin
.from("appointments")
Expand Down
33 changes: 29 additions & 4 deletions app/api/admin/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { supabaseAdmin } from "@/lib/supabase-admin";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

const JWT_SECRET = process.env.JWT_SECRET || "default-secret";

export async function POST(req: NextRequest) {
try {
Expand All @@ -9,16 +13,37 @@ export async function POST(req: NextRequest) {
.from("system_admins")
.select("*")
.eq("email", email)
.eq("password", password)
.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 } });
const isMatch = await bcrypt.compare(password, admin.password);
if (!isMatch) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}

const token = jwt.sign(
{ email: admin.email, role: "admin" },
JWT_SECRET,
{ expiresIn: "8h" }
);

const response = NextResponse.json({
message: "Login successful",
admin: { email: admin.email }
});

response.cookies.set("admin_session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 8 * 60 * 60, // 8 hours
path: "/",
});

return response;
} catch (err) {
console.error("Admin login error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
Expand Down
12 changes: 8 additions & 4 deletions app/api/appointments/book/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ export async function POST(req: NextRequest) {
}
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
// We add .eq("is_booked", false) to ensure we only book if it's still available (Optimistic Concurrency)
const { data: updatedSlot, error: slotUpdateError } = await supabaseAdmin
.from("slots")
.update({ is_booked: true })
.eq("id", slotId);
.eq("id", slotId)
.eq("is_booked", false)
.select();

if (slotUpdateError) {
return NextResponse.json({ error: "Failed to update slot" }, { status: 500 });
if (slotUpdateError || !updatedSlot || updatedSlot.length === 0) {
return NextResponse.json({ error: "Slot is no longer available" }, { status: 409 });
}

const { error: insertError } = await supabaseAdmin.from("appointments").insert({
Expand All @@ -40,6 +43,7 @@ export async function POST(req: NextRequest) {
});

if (insertError) {
console.error("Appointment insertion failed, rolling back slot update:", 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 });
Expand Down
165 changes: 164 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.49.4",
"bcryptjs": "^3.0.3",
"jsonwebtoken": "^9.0.3",
"next": "^15.5.15",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand All @@ -20,6 +22,8 @@
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
Loading