fitaiProto/apps/admin/src/app/api/users/create/route.ts
2026-03-11 01:43:19 +01:00

160 lines
4.7 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { auth, clerkClient } from "@clerk/nextjs/server";
import { db, users, clients } from "@fitai/database";
import { createUserSchema } from "@/lib/validation/user-schemas";
import { sendWelcomeEmail, sendInvitationEmail } from "@/lib/email";
import {
successResponse,
errorResponse,
unauthorizedResponse,
} from "@/lib/api/responses";
import log from "@/lib/logger";
/**
* POST /api/users/create
*
* Create a new user with support for:
* - Direct creation (database only, no Clerk account)
* - Invitation-based creation (sends Clerk invitation email)
* - Client-specific fields (membership type, status)
* - Welcome emails for clients
*/
export async function POST(request: NextRequest) {
try {
const { userId } = await auth();
if (!userId) {
return unauthorizedResponse("Unauthorized");
}
const body = await request.json();
const validationResult = createUserSchema.safeParse(body);
if (!validationResult.success) {
return errorResponse("Validation failed", {
status: 400,
details: validationResult.error.flatten().fieldErrors as Record<
string,
string[]
>,
});
}
const data = validationResult.data;
// Note: Email is required in current schema
// TODO: Add schema migration to make email optional for direct creation
if (!data.email) {
return errorResponse("Email is required", { status: 400 });
}
// Case 1: Send Clerk Invitation (requires email)
if (data.sendInvitation && data.email) {
try {
const client = await clerkClient();
const invitation = await client.invitations.createInvitation({
emailAddress: data.email,
publicMetadata: {
role: data.role,
gymId: data.gymId,
},
});
log.info("Clerk invitation sent", {
email: data.email,
role: data.role,
invitationId: invitation.id,
});
// Send custom invitation email (in addition to Clerk's)
if (invitation.url) {
await sendInvitationEmail(data.email, data.role, invitation.url);
}
return successResponse(
{
invitation: {
id: invitation.id,
email: invitation.emailAddress,
status: invitation.status,
},
},
{ status: 200 },
);
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
log.error("Failed to send Clerk invitation", error);
return errorResponse(`Failed to send invitation: ${errorMessage}`, {
status: 500,
});
}
}
// Case 2: Direct Creation (database only, no Clerk account)
// This creates a user record that can later be linked when they sign up
// Generate a temporary user ID (will be replaced when they sign up with Clerk)
const tempUserId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Insert user into database
const [newUser] = await db
.insert(users)
.values({
id: tempUserId,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
role: data.role,
phone: data.phone,
gymId: data.gymId,
})
.returning();
log.info("User created in database", {
userId: newUser.id,
role: data.role,
hasEmail: !!data.email,
});
// Case 3: Create client record if role is client
if (data.role === "client" && data.membershipType) {
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
await db.insert(clients).values({
id: clientId,
userId: newUser.id,
membershipType: data.membershipType,
membershipStatus: data.membershipStatus || "active",
joinDate: new Date(),
});
log.info("Client record created", {
userId: newUser.id,
membershipType: data.membershipType,
});
// Send welcome email if requested and email provided
if (data.sendWelcomeEmail && data.email) {
await sendWelcomeEmail(data.email, data.firstName);
log.info("Welcome email sent", { email: data.email });
}
}
return successResponse(
{
user: {
id: newUser.id,
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
role: newUser.role,
},
},
{ status: 201 },
);
} catch (error) {
log.error("Failed to create user", error);
return errorResponse("Failed to create user", { status: 500 });
}
}