260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getDatabase } from "../../../lib/database/index";
|
|
import bcrypt from "bcryptjs";
|
|
import { auth, clerkClient } from "@clerk/nextjs/server";
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const db = await getDatabase();
|
|
const { searchParams } = new URL(request.url);
|
|
const role = searchParams.get("role");
|
|
|
|
let users = await db.getAllUsers();
|
|
|
|
if (role) {
|
|
users = users.filter((user) => user.role === role);
|
|
}
|
|
|
|
const usersWithClients = await Promise.all(
|
|
users.map(async (user) => {
|
|
const { password: _, ...userWithoutPassword } = user;
|
|
const client = await db.getClientByUserId(user.id);
|
|
|
|
// Get active check-in status
|
|
let isCheckedIn = false;
|
|
let checkInTime = null;
|
|
|
|
if (client) {
|
|
const activeCheckIn = await db.getActiveCheckIn(client.id);
|
|
if (activeCheckIn) {
|
|
isCheckedIn = true;
|
|
checkInTime = activeCheckIn.checkInTime;
|
|
}
|
|
}
|
|
|
|
return {
|
|
...userWithoutPassword,
|
|
client,
|
|
isCheckedIn,
|
|
checkInTime
|
|
};
|
|
}),
|
|
);
|
|
|
|
return NextResponse.json({ users: usersWithClients });
|
|
} catch (error) {
|
|
console.error("Get users error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const { userId: clerkUserId } = await auth();
|
|
if (!clerkUserId) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const db = await getDatabase();
|
|
|
|
// Get current user to check role
|
|
// Note: In a real app, we'd map Clerk ID to our DB ID.
|
|
// For now, we'll assume we can find the user by some means or trust the Clerk metadata if we synced it.
|
|
// Since we don't have Clerk ID in our local DB users table yet (we only have our own ID),
|
|
// we might need to rely on the user being synced.
|
|
// Let's assume the user calling this API is already in our DB.
|
|
// For the prototype, we'll fetch the user by matching the Clerk ID if we stored it,
|
|
// OR we'll assume the first user is Super Admin if no users exist?
|
|
// Actually, we should look up the user by email if we can't by ID, or add a clerkId column.
|
|
// For this step, let's assume we can get the user.
|
|
|
|
// WAIT: The current `users` table has `id` as a string. Is it the Clerk ID?
|
|
// In `sync-user.ts`, we use `evt.data.id` as the `id` when creating the user.
|
|
// So yes, `users.id` IS the Clerk ID.
|
|
|
|
const currentUser = await db.getUserById(clerkUserId);
|
|
|
|
if (!currentUser) {
|
|
return NextResponse.json({ error: "Current user not found in database" }, { status: 403 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { email, firstName, lastName, role, phone } = body;
|
|
|
|
if (!email || !firstName || !lastName || !role) {
|
|
return NextResponse.json(
|
|
{ error: "Missing required fields" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Enforce Hierarchy
|
|
const allowed: Record<string, string[]> = {
|
|
superAdmin: ["admin", "trainer", "client"],
|
|
admin: ["trainer", "client"],
|
|
trainer: ["client"],
|
|
client: []
|
|
};
|
|
|
|
const userRole = currentUser.role as keyof typeof allowed;
|
|
if (!allowed[userRole] || !allowed[userRole].includes(role)) {
|
|
return NextResponse.json(
|
|
{ error: `You are not authorized to create a ${role}` },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
// Check if user already exists locally
|
|
const existingUser = await db.getUserByEmail(email);
|
|
if (existingUser) {
|
|
return NextResponse.json(
|
|
{ error: "User with this email already exists" },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
|
|
// Create Clerk Invitation
|
|
// Note: We pass the role in publicMetadata so it persists when they sign up
|
|
try {
|
|
const client = await clerkClient();
|
|
await client.invitations.createInvitation({
|
|
emailAddress: email,
|
|
publicMetadata: {
|
|
role,
|
|
},
|
|
ignoreExisting: true // Don't fail if invite exists
|
|
});
|
|
} catch (clerkError: any) {
|
|
console.error("Clerk invitation error:", clerkError);
|
|
// If user already exists in Clerk, we might want to handle it.
|
|
// But for now, let's proceed to create local record if invite sent or if they exist.
|
|
if (clerkError.errors?.[0]?.code === 'form_identifier_exists') {
|
|
return NextResponse.json(
|
|
{ error: "User already exists in Clerk system" },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
return NextResponse.json(
|
|
{ error: "Failed to send invitation: " + (clerkError.message || "Unknown error") },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
// Create user in local DB with temporary ID (will be migrated on first login)
|
|
// We set a placeholder password since it's required by schema but won't be used
|
|
const newUserId = await db.createUser({
|
|
email,
|
|
password: "INVITED_USER_PENDING",
|
|
firstName,
|
|
lastName,
|
|
role,
|
|
phone,
|
|
});
|
|
|
|
// If creating a client, create the client record too
|
|
if (role === 'client') {
|
|
await db.createClient({
|
|
userId: newUserId.id,
|
|
membershipType: 'basic',
|
|
membershipStatus: 'active',
|
|
joinDate: new Date()
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({ userId: newUserId.id, message: "Invitation sent" }, { status: 201 });
|
|
} catch (error) {
|
|
console.error("Create user error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function PUT(request: NextRequest) {
|
|
try {
|
|
const db = await getDatabase();
|
|
const body = await request.json();
|
|
const { id, email, firstName, lastName, role, phone } = body;
|
|
|
|
if (!id) {
|
|
return NextResponse.json(
|
|
{ error: "User ID is required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Get existing user
|
|
const existingUser = await db.getUserById(id);
|
|
if (!existingUser) {
|
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
}
|
|
|
|
// Check if email is being changed and if it's already taken
|
|
if (email && email !== existingUser.email) {
|
|
const userWithEmail = await db.getUserByEmail(email);
|
|
if (userWithEmail) {
|
|
return NextResponse.json(
|
|
{ error: "Email already in use" },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// Update user
|
|
await db.updateUser(id, {
|
|
email: email || existingUser.email,
|
|
firstName: firstName || existingUser.firstName,
|
|
lastName: lastName || existingUser.lastName,
|
|
role: role || existingUser.role,
|
|
phone: phone !== undefined ? phone : existingUser.phone,
|
|
});
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Update user error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function DELETE(request: NextRequest) {
|
|
try {
|
|
const db = await getDatabase();
|
|
const { searchParams } = new URL(request.url);
|
|
const id = searchParams.get("id");
|
|
const body = await request.json().catch(() => ({}));
|
|
const { ids } = body;
|
|
|
|
if (ids && Array.isArray(ids)) {
|
|
// Bulk delete
|
|
await Promise.all(ids.map((userId: string) => db.deleteUser(userId)));
|
|
return NextResponse.json({ success: true, deleted: ids.length });
|
|
} else if (id) {
|
|
// Single delete
|
|
const user = await db.getUserById(id);
|
|
if (!user) {
|
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
}
|
|
await db.deleteUser(id);
|
|
return NextResponse.json({ success: true });
|
|
} else {
|
|
return NextResponse.json(
|
|
{ error: "User ID or IDs array required" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Delete user error:", error);
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|