Dental Care
This commit is contained in:
75
app/api/appointments/[id]/edit/route.ts
Normal file
75
app/api/appointments/[id]/edit/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await import("next/headers").then((mod) => mod.headers()),
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin can edit appointments
|
||||
if (session.user.role !== "admin") {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden: Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { date, timeSlot, status, notes } = body;
|
||||
|
||||
// Validate that the appointment exists
|
||||
const existingAppointment = await prisma.appointment.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingAppointment) {
|
||||
return NextResponse.json(
|
||||
{ error: "Appointment not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build update data object
|
||||
const updateData: Prisma.AppointmentUpdateInput = {};
|
||||
|
||||
if (date !== undefined) updateData.date = new Date(date);
|
||||
if (timeSlot !== undefined) updateData.timeSlot = timeSlot;
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
// Update the appointment
|
||||
const updatedAppointment = await prisma.appointment.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
payment: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
appointment: updatedAppointment,
|
||||
message: "Appointment updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating appointment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update appointment" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
139
app/api/appointments/[id]/route.ts
Normal file
139
app/api/appointments/[id]/route.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { status, cancelReason, date, timeSlot } = body;
|
||||
const { id } = await params;
|
||||
|
||||
const appointment = await prisma.appointment.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!appointment) {
|
||||
return NextResponse.json(
|
||||
{ error: "Appointment not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update appointment
|
||||
const updatedAppointment = await prisma.appointment.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(status && { status }),
|
||||
...(cancelReason && { cancelReason }),
|
||||
...(date && { date: new Date(date) }),
|
||||
...(timeSlot && { timeSlot }),
|
||||
},
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create notifications based on action
|
||||
if (status === "cancelled") {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.patientId,
|
||||
title: "Appointment Cancelled",
|
||||
message: `Your appointment for ${appointment.service.name} on ${new Date(appointment.date).toLocaleDateString()} has been cancelled.`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.dentistId,
|
||||
title: "Appointment Cancelled",
|
||||
message: `Appointment with ${appointment.patient.name} on ${new Date(appointment.date).toLocaleDateString()} has been cancelled.`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
} else if (status === "confirmed") {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.patientId,
|
||||
title: "Appointment Confirmed",
|
||||
message: `Your appointment for ${appointment.service.name} on ${new Date(appointment.date).toLocaleDateString()} has been confirmed.`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
} else if (date || timeSlot) {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.patientId,
|
||||
title: "Appointment Rescheduled",
|
||||
message: `Your appointment has been rescheduled to ${new Date(updatedAppointment.date).toLocaleDateString()} at ${updatedAppointment.timeSlot}.`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.dentistId,
|
||||
title: "Appointment Rescheduled",
|
||||
message: `Appointment with ${appointment.patient.name} has been rescheduled to ${new Date(updatedAppointment.date).toLocaleDateString()} at ${updatedAppointment.timeSlot}.`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(updatedAppointment);
|
||||
} catch (error) {
|
||||
console.error("Error updating appointment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update appointment" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
await prisma.appointment.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Appointment deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting appointment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete appointment" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
270
app/api/appointments/book/route.ts
Normal file
270
app/api/appointments/book/route.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
import { Resend } from "resend";
|
||||
import { createElement } from "react";
|
||||
import DentalInvoice from "@/components/emails/email-bookings";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { appointmentData } = body;
|
||||
|
||||
if (!appointmentData) {
|
||||
return NextResponse.json(
|
||||
{ error: "Appointment data is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
interface ServiceItem {
|
||||
qty: number;
|
||||
description: string;
|
||||
unitPrice: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
const { patientId, personalInfo, appointment, services, specialRequests } =
|
||||
appointmentData as {
|
||||
patientId: string;
|
||||
personalInfo: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
contactNumber?: string;
|
||||
};
|
||||
appointment: {
|
||||
dentistId: string;
|
||||
dentistName: string;
|
||||
date: string;
|
||||
time: string;
|
||||
};
|
||||
services: ServiceItem[];
|
||||
specialRequests: string;
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!patientId ||
|
||||
!appointment.dentistId ||
|
||||
!appointment.date ||
|
||||
!appointment.time
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required appointment fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if time slot is already booked
|
||||
const existingAppointment = await prisma.appointment.findFirst({
|
||||
where: {
|
||||
dentistId: appointment.dentistId,
|
||||
date: new Date(appointment.date),
|
||||
timeSlot: appointment.time,
|
||||
status: {
|
||||
in: ["pending", "confirmed"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAppointment) {
|
||||
return NextResponse.json(
|
||||
{ error: "This time slot is already booked" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create appointments for each service
|
||||
const createdAppointments = [];
|
||||
|
||||
for (const service of services) {
|
||||
if (service.qty > 0 && service.description) {
|
||||
// Find the service in database
|
||||
const dbService = await prisma.service.findFirst({
|
||||
where: { name: service.description },
|
||||
});
|
||||
|
||||
if (dbService) {
|
||||
const newAppointment = await prisma.appointment.create({
|
||||
data: {
|
||||
patientId,
|
||||
dentistId: appointment.dentistId,
|
||||
serviceId: dbService.id,
|
||||
date: new Date(appointment.date),
|
||||
timeSlot: appointment.time,
|
||||
notes: specialRequests || null,
|
||||
status: "pending",
|
||||
},
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
},
|
||||
});
|
||||
|
||||
createdAppointments.push(newAppointment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (createdAppointments.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No valid services selected" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Send confirmation email to patient with professional invoice template
|
||||
// Generate invoice number
|
||||
const invoiceNumber = `INV-${Date.now()}-${Math.random().toString(36).substring(7).toUpperCase()}`;
|
||||
const invoiceDate = new Date().toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const dueDate = new Date(
|
||||
Date.now() + 30 * 24 * 60 * 60 * 1000
|
||||
).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
// Format appointment date and time
|
||||
const formattedAppointmentDate = new Date(
|
||||
appointment.date
|
||||
).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
// Calculate next appointment (6 months from now for regular checkup)
|
||||
const nextApptDate = new Date(appointment.date);
|
||||
nextApptDate.setMonth(nextApptDate.getMonth() + 6);
|
||||
const nextAppointmentDate = nextApptDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
// Calculate total duration (assuming each service takes 60 minutes)
|
||||
const totalDuration = services
|
||||
.filter((s) => s.qty > 0)
|
||||
.reduce((sum, s) => sum + s.qty * 60, 0);
|
||||
|
||||
// Calculate financial totals
|
||||
const subtotal = services
|
||||
.filter((s) => s.qty > 0)
|
||||
.reduce((sum, s) => sum + s.total, 0);
|
||||
const tax = subtotal * 0.12; // 12% tax
|
||||
const totalDue = subtotal + tax;
|
||||
|
||||
// Filter services with qty > 0 for email
|
||||
const activeServices = services.filter((s) => s.qty > 0);
|
||||
|
||||
try {
|
||||
console.log("Attempting to send email to:", personalInfo.email);
|
||||
console.log(
|
||||
"From address:",
|
||||
`${process.env.EMAIL_SENDER_NAME} <${process.env.EMAIL_SENDER_ADDRESS}>`
|
||||
);
|
||||
|
||||
const emailResult = await resend.emails.send({
|
||||
from: `${process.env.EMAIL_SENDER_NAME} <${process.env.EMAIL_SENDER_ADDRESS}>`,
|
||||
to: personalInfo.email,
|
||||
subject: `Appointment Confirmation - Invoice #${invoiceNumber}`,
|
||||
react: createElement(DentalInvoice, {
|
||||
invoiceNumber,
|
||||
invoiceDate,
|
||||
dueDate,
|
||||
patientName: `${personalInfo.firstName} ${personalInfo.lastName}`,
|
||||
patientAddress: personalInfo.address || "N/A",
|
||||
patientCity: personalInfo.city || "N/A",
|
||||
patientPhone: personalInfo.contactNumber || "N/A",
|
||||
patientEmail: personalInfo.email,
|
||||
bookingId: createdAppointments[0]?.id || "PENDING",
|
||||
appointmentDate: formattedAppointmentDate,
|
||||
appointmentTime: appointment.time,
|
||||
doctorName: appointment.dentistName,
|
||||
treatmentRoom: "Room 1",
|
||||
appointmentDuration: `${totalDuration} minutes`,
|
||||
reasonForVisit:
|
||||
specialRequests ||
|
||||
services
|
||||
.filter((s) => s.qty > 0)
|
||||
.map((s) => s.description)
|
||||
.join(", "),
|
||||
pdfDownloadUrl: `${process.env.NEXT_PUBLIC_APP_URL}/patient/appointments`,
|
||||
paymentStatus: "Pending Payment",
|
||||
nextAppointmentDate,
|
||||
nextAppointmentTime: appointment.time,
|
||||
nextAppointmentPurpose: "Regular Dental Checkup & Cleaning",
|
||||
services: activeServices,
|
||||
subtotal,
|
||||
tax,
|
||||
totalDue,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Email sent successfully:", emailResult);
|
||||
} catch (emailError) {
|
||||
console.error("Error sending email:", emailError);
|
||||
console.error(
|
||||
"Email error details:",
|
||||
JSON.stringify(emailError, null, 2)
|
||||
);
|
||||
// Don't fail the appointment creation if email fails
|
||||
}
|
||||
|
||||
// Create notification for patient
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: patientId,
|
||||
title: "Appointment Booked",
|
||||
message: `Your appointment with Dr. ${appointment.dentistName} has been booked for ${new Date(appointment.date).toLocaleDateString()} at ${appointment.time}`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
// Create notification for dentist
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: appointment.dentistId,
|
||||
title: "New Appointment",
|
||||
message: `New appointment request from ${personalInfo.firstName} ${personalInfo.lastName} for ${new Date(appointment.date).toLocaleDateString()} at ${appointment.time}`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
appointments: createdAppointments,
|
||||
message:
|
||||
"Appointment booked successfully! Check your email for confirmation.",
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error booking appointment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to book appointment" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
165
app/api/appointments/route.ts
Normal file
165
app/api/appointments/route.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
import { safeFindManyAppointments } from "@/lib/utils/appointment-helpers";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { patientId, dentistId, serviceId, date, timeSlot, notes } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!patientId || !dentistId || !serviceId || !date || !timeSlot) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if time slot is already booked
|
||||
const existingAppointment = await prisma.appointment.findFirst({
|
||||
where: {
|
||||
dentistId,
|
||||
date: new Date(date),
|
||||
timeSlot,
|
||||
status: {
|
||||
in: ["pending", "confirmed"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAppointment) {
|
||||
return NextResponse.json(
|
||||
{ error: "This time slot is already booked" },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create appointment
|
||||
const appointment = await prisma.appointment.create({
|
||||
data: {
|
||||
patientId,
|
||||
dentistId,
|
||||
serviceId,
|
||||
date: new Date(date),
|
||||
timeSlot,
|
||||
notes: notes || null,
|
||||
status: "pending",
|
||||
},
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create notification for patient
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: patientId,
|
||||
title: "Appointment Booked",
|
||||
message: `Your appointment for ${appointment.service.name} has been booked for ${new Date(date).toLocaleDateString()} at ${timeSlot}`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
// Create notification for dentist
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: dentistId,
|
||||
title: "New Appointment",
|
||||
message: `New appointment request from ${appointment.patient.name} for ${new Date(date).toLocaleDateString()} at ${timeSlot}`,
|
||||
type: "email",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(appointment, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating appointment:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create appointment" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const userId = searchParams.get("userId");
|
||||
const role = searchParams.get("role");
|
||||
|
||||
let appointments;
|
||||
|
||||
if (role === "patient") {
|
||||
appointments = await safeFindManyAppointments({
|
||||
take: 100, // Limit to 100 most recent appointments
|
||||
where: {
|
||||
patientId: userId || session.user.id,
|
||||
},
|
||||
include: {
|
||||
dentist: true,
|
||||
service: true,
|
||||
payment: true,
|
||||
},
|
||||
orderBy: {
|
||||
date: "desc",
|
||||
},
|
||||
});
|
||||
} else if (role === "dentist") {
|
||||
appointments = await safeFindManyAppointments({
|
||||
take: 100, // Limit to 100 most recent appointments
|
||||
where: {
|
||||
dentistId: userId || session.user.id,
|
||||
},
|
||||
include: {
|
||||
patient: true,
|
||||
service: true,
|
||||
payment: true,
|
||||
},
|
||||
orderBy: {
|
||||
date: "desc",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Admin - get all appointments with pagination limit
|
||||
// Use safe find to filter out orphaned appointments
|
||||
appointments = await safeFindManyAppointments({
|
||||
take: 100, // Limit to 100 most recent appointments
|
||||
include: {
|
||||
patient: true,
|
||||
dentist: true,
|
||||
service: true,
|
||||
payment: true,
|
||||
},
|
||||
orderBy: {
|
||||
date: "desc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(appointments);
|
||||
} catch (error) {
|
||||
console.error("Error fetching appointments:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch appointments" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
4
app/api/auth/[...all]/route.ts
Normal file
4
app/api/auth/[...all]/route.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth.handler);
|
||||
33
app/api/auth/resend-verification/route.ts
Normal file
33
app/api/auth/resend-verification/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
|
||||
/**
|
||||
* POST /api/auth/resend-verification
|
||||
* Resends the email verification link to the user
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: "Email is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use Better Auth's sendVerificationEmail method
|
||||
await auth.api.sendVerificationEmail({
|
||||
body: { email },
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Verification email sent successfully" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to resend verification email:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to send verification email" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/api/auth/session/route.ts
Normal file
50
app/api/auth/session/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "No session found" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Fetch the full user object from database to get the role
|
||||
// This is necessary because session cache doesn't include additional fields
|
||||
if (session.user) {
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dbUser) {
|
||||
// Merge the role from database into the session user object
|
||||
session.user = {
|
||||
...session.user,
|
||||
role: dbUser.role || "patient", // Default to patient if no role set
|
||||
};
|
||||
} else {
|
||||
// Fallback if user not found in database
|
||||
session.user.role = "patient";
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(session);
|
||||
} catch (error) {
|
||||
console.error("Session fetch error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
78
app/api/debug-session/route.ts
Normal file
78
app/api/debug-session/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
|
||||
/**
|
||||
* Debug endpoint to check session and cookie status
|
||||
* Access this at: /api/debug-session
|
||||
*
|
||||
* REMOVE THIS FILE AFTER DEBUGGING
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Get all cookies
|
||||
const allCookies = request.cookies.getAll();
|
||||
const sessionToken = request.cookies.get("better-auth.session_token");
|
||||
|
||||
// Try to get session from Better Auth
|
||||
let session = null;
|
||||
let sessionError = null;
|
||||
try {
|
||||
session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
} catch (error) {
|
||||
sessionError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
// Get request details
|
||||
const info = {
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: {
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
hasAuthUrl: !!process.env.BETTER_AUTH_URL,
|
||||
authUrl: process.env.BETTER_AUTH_URL,
|
||||
hasAppUrl: !!process.env.NEXT_PUBLIC_APP_URL,
|
||||
appUrl: process.env.NEXT_PUBLIC_APP_URL,
|
||||
},
|
||||
request: {
|
||||
url: request.url,
|
||||
origin: request.headers.get("origin"),
|
||||
referer: request.headers.get("referer"),
|
||||
host: request.headers.get("host"),
|
||||
protocol: request.headers.get("x-forwarded-proto") || "unknown",
|
||||
},
|
||||
cookies: {
|
||||
total: allCookies.length,
|
||||
names: allCookies.map((c) => c.name),
|
||||
hasSessionToken: !!sessionToken,
|
||||
sessionTokenValue: sessionToken?.value
|
||||
? `${sessionToken.value.substring(0, 20)}...`
|
||||
: null,
|
||||
},
|
||||
session: session
|
||||
? {
|
||||
userId: session.user?.id,
|
||||
userEmail: session.user?.email,
|
||||
sessionId: session.session?.id,
|
||||
expiresAt: session.session?.expiresAt,
|
||||
}
|
||||
: null,
|
||||
sessionError,
|
||||
};
|
||||
|
||||
return NextResponse.json(info, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
49
app/api/send-appointment-reminder/route.tsx
Normal file
49
app/api/send-appointment-reminder/route.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import DentalAppointmentReminder from "@/components/emails/email-remainder";
|
||||
import { Resend } from "resend";
|
||||
import React from "react";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const {
|
||||
patientName,
|
||||
appointmentDate,
|
||||
appointmentTime,
|
||||
doctorName,
|
||||
treatmentType,
|
||||
duration,
|
||||
clinicPhone,
|
||||
clinicEmail,
|
||||
clinicAddress,
|
||||
to,
|
||||
} = body;
|
||||
|
||||
try {
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: `Dental U Care <${process.env.EMAIL_SENDER_ADDRESS || "onboarding@dentalucare.tech"}>`,
|
||||
to: [to],
|
||||
subject: `Dental Appointment Reminder for ${patientName}`,
|
||||
react: (
|
||||
<DentalAppointmentReminder
|
||||
patientName={patientName}
|
||||
appointmentDate={appointmentDate}
|
||||
appointmentTime={appointmentTime}
|
||||
doctorName={doctorName}
|
||||
treatmentType={treatmentType}
|
||||
duration={duration}
|
||||
clinicPhone={clinicPhone}
|
||||
clinicEmail={clinicEmail}
|
||||
clinicAddress={clinicAddress || ""}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return Response.json({ error }, { status: 500 });
|
||||
}
|
||||
return Response.json(data);
|
||||
} catch (error) {
|
||||
return Response.json({ error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
73
app/api/users/[id]/role/route.ts
Normal file
73
app/api/users/[id]/role/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/types/prisma";
|
||||
import { auth } from "@/lib/auth-session/auth";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await import("next/headers").then((mod) => mod.headers()),
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Only admin can change roles
|
||||
if (session.user.role !== "admin") {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden: Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { role } = body;
|
||||
|
||||
// Validate role
|
||||
if (!role || !["patient", "dentist", "admin"].includes(role)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid role. Must be patient, dentist, or admin" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Prevent changing own role
|
||||
if (user.id === session.user.id) {
|
||||
return NextResponse.json(
|
||||
{ error: "You cannot change your own role" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Update user role
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { role },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: updatedUser,
|
||||
message: `Role changed to ${role} successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing user role:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to change user role" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user