import { NextRequest, NextResponse } from "next/server"; import { getDatabase } from "../../../lib/database/index"; import bcrypt from "bcryptjs"; import { auth, clerkClient } from "@clerk/nextjs/server"; import { db as rawDb, sql } from "@fitai/database"; 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(); // Hydrate gymId from raw DB to ensure consistency with writes const rawUserRows = await rawDb.all(sql`SELECT id, gym_id FROM users`); const gymById = new Map( (rawUserRows || []).map((r: any) => [ r.id as string, (r.gym_id as string | null) ?? null, ]), ); // Load gym names for mapping gymId -> gymName const gymRows = await rawDb.all(sql`SELECT id, name FROM gyms`); const gymNames = new Map( (gymRows || []) .filter((g: any) => !!g && typeof g.id === "string") .map((g: any) => [ g.id as string, (g.name as string) || (g.id as string), ]), ); console.log( "GET /api/users: total users fetched from DB:", Array.isArray(users) ? users.length : 0, ); if (role) { users = users.filter((user) => user.role === role); } console.log( "GET /api/users: role filter:", role, "users after filter:", Array.isArray(users) ? users.length : 0, "sample:", users && users[0] ? { id: users[0].id, role: users[0].role, gymId: (users as any)[0].gymId, } : null, ); 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 and statistics for ALL users let isCheckedIn = false; let checkInTime = null; let lastCheckInTime = null; let checkInsThisWeek = 0; let checkInsThisMonth = 0; // Query attendance by userId (works for all user types now) const activeCheckIn = await db.getActiveCheckIn(user.id); if (activeCheckIn) { isCheckedIn = true; checkInTime = activeCheckIn.checkInTime; } // Get attendance history for statistics const attendanceHistory = await db.getAttendanceHistory(user.id); if (attendanceHistory.length > 0) { // Last check-in is the most recent attendance record lastCheckInTime = attendanceHistory[0].checkInTime; // Calculate check-ins in last 7 days const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); checkInsThisWeek = attendanceHistory.filter( (a) => new Date(a.checkInTime) >= weekAgo, ).length; // Calculate check-ins in last 30 days const monthAgo = new Date(); monthAgo.setDate(monthAgo.getDate() - 30); checkInsThisMonth = attendanceHistory.filter( (a) => new Date(a.checkInTime) >= monthAgo, ).length; } return { ...userWithoutPassword, // Override gymId from raw DB hydration to avoid undefined from Drizzle mapping gymId: gymById.get(user.id) ?? (user as any).gymId ?? undefined, // Provide gymName mapped from gyms table gymName: (() => { const gid = gymById.get(user.id) ?? (user as any).gymId ?? undefined; if (!gid) return null; return gymNames.get(gid) ?? null; })(), client, isCheckedIn, checkInTime, lastCheckInTime, checkInsThisWeek, checkInsThisMonth, }; }), ); console.log( "GET /api/users: responding users count:", Array.isArray(usersWithClients) ? usersWithClients.length : 0, "sample:", usersWithClients && usersWithClients[0] ? { id: usersWithClients[0].id, role: usersWithClients[0].role, gymId: (usersWithClients as any)[0].gymId, } : null, ); 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 = { 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, gymId } = body; console.log("PUT /api/users received body:", { id, email, firstName, lastName, role, phone, gymId, }); if (!id) { return NextResponse.json( { error: "User ID is required" }, { status: 400 }, ); } // Authenticate requester const { userId: requesterId } = await auth(); if (!requesterId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Fetch requester and target user const requester = await db.getUserById(requesterId); if (!requester) { return NextResponse.json( { error: "Requester not found" }, { status: 403 }, ); } const existingUser = await db.getUserById(id); if (!existingUser) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } // Authorization: determine allowed role changes const requesterRole = requester.role; const allowedByRole: Record = { superAdmin: ["superAdmin", "admin", "trainer", "client", "generalUser"], admin: ["admin", "trainer", "client", "generalUser"], trainer: [], // trainers cannot change roles client: [], // clients cannot change roles generalUser: [], // general users cannot change roles }; if (role && !allowedByRole[requesterRole]?.includes(role)) { return NextResponse.json( { error: `Not authorized to assign role '${role}'` }, { status: 403 }, ); } // 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 Clerk publicMetadata (role/gymId) to propagate via webhook // Note: Only update metadata when a change is requested try { const client = await clerkClient(); const publicMetadata: Record = {}; console.log("PUT /api/users preparing Clerk metadata update:", { targetUserId: id, role, gymId, }); if (role) { publicMetadata.role = role; } if (gymId !== undefined) { publicMetadata.gymId = gymId === null ? null : String(gymId); } if (Object.keys(publicMetadata).length > 0) { console.log( "PUT /api/users calling Clerk updateUser with metadata:", publicMetadata, ); const clerkResult = await client.users.updateUser(id, { publicMetadata, }); console.log("PUT /api/users Clerk updateUser result:", { id: clerkResult.id, role: clerkResult.publicMetadata?.role, gymId: clerkResult.publicMetadata?.gymId, }); } else { console.log("PUT /api/users no Clerk metadata changes requested"); } } catch (clerkErr: any) { console.error("Clerk metadata update error:", clerkErr); return NextResponse.json( { error: "Failed to update role/gym in identity provider" }, { status: 500 }, ); } // Update local DB for immediate UI feedback (webhook will also sync) console.log( "PUT /api/users raw SQL updating local DB user gym_id and fields", ); await rawDb.run( sql`UPDATE users SET email = ${email ?? existingUser.email}, first_name = ${firstName ?? existingUser.firstName}, last_name = ${lastName ?? existingUser.lastName}, role = ${role ?? existingUser.role}, phone = ${phone !== undefined && typeof phone === "string" ? phone : (existingUser.phone ?? null)}, gym_id = ${gymId !== undefined ? gymId : (existingUser.gymId ?? null)}, updated_at = ${Date.now()} WHERE id = ${id}`, ); // Read back the updated row to surface gym_id and confirm write const updatedRow = await rawDb.get( sql`SELECT id, email, first_name, last_name, role, phone, gym_id, created_at, updated_at FROM users WHERE id = ${id}`, ); console.log("PUT /api/users raw DB row after update:", updatedRow); const updatedUser = { ...existingUser, email: email ?? existingUser.email, firstName: firstName ?? existingUser.firstName, lastName: lastName ?? existingUser.lastName, role: role ?? existingUser.role, phone: phone !== undefined ? phone : existingUser.phone, gymId: updatedRow?.gym_id !== undefined ? updatedRow.gym_id : gymId !== undefined ? gymId : existingUser.gymId, }; console.log("PUT /api/users responding with updated user:", updatedUser); return NextResponse.json({ user: updatedUser }); } 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 }, ); } }