diff --git a/apps/admin/.env.example b/apps/admin/.env.example index a852574..41eae41 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -14,6 +14,10 @@ RESEND_API_KEY=re_your_resend_api_key_here EMAIL_FROM=FitAI EMAIL_REPLY_TO=support@yourdomain.com +# Admin App URL (for invitation redirects) +# Set to your production URL in production +NEXT_PUBLIC_APP_URL=http://localhost:3000 + # Database (optional - defaults to ./fitai.db) DATABASE_PATH=./fitai.db diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index f5a3cd0..ed0cb0c 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/admin/src/app/api/users/create/route.ts b/apps/admin/src/app/api/users/create/route.ts index fe2152d..1ab24bb 100644 --- a/apps/admin/src/app/api/users/create/route.ts +++ b/apps/admin/src/app/api/users/create/route.ts @@ -84,12 +84,30 @@ export async function POST(request: NextRequest) { if (data.sendInvitation && data.email) { try { const client = await clerkClient(); + + // Build publicMetadata - only include gymId if it exists + const publicMetadata: Record = { + role: data.role, + }; + if (assignedGymId) { + publicMetadata.gymId = assignedGymId; + } + + // Determine redirect URL based on role + // Staff (admin, trainer, superAdmin) → Admin web app + // Clients → Mobile app (handled by Clerk's default) + const isStaffRole = ["admin", "trainer", "superAdmin"].includes( + data.role, + ); + const redirectUrl = isStaffRole + ? `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/sign-up` + : undefined; // Clients use default Clerk redirect + const invitation = await client.invitations.createInvitation({ emailAddress: data.email, - publicMetadata: { - role: data.role, - gymId: assignedGymId, - }, + publicMetadata, + redirectUrl, + ignoreExisting: true, // Don't fail if invitation already exists }); log.info("Clerk invitation sent", { @@ -97,6 +115,7 @@ export async function POST(request: NextRequest) { role: data.role, gymId: assignedGymId, invitationId: invitation.id, + redirectUrl: redirectUrl || "default (mobile app)", }); // Send custom invitation email (in addition to Clerk's) @@ -117,7 +136,22 @@ export async function POST(request: NextRequest) { } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; - log.error("Failed to send Clerk invitation", error); + + // Extract Clerk-specific error details + const clerkError = error as any; + + log.error("Failed to send Clerk invitation", error, { + email: data.email, + role: data.role, + gymId: assignedGymId, + publicMetadata: assignedGymId + ? { role: data.role, gymId: assignedGymId } + : { role: data.role }, + clerkStatus: clerkError?.status, + clerkErrors: clerkError?.errors, + clerkTraceId: clerkError?.clerkTraceId, + }); + return errorResponse(`Failed to send invitation: ${errorMessage}`, { status: 500, }); diff --git a/apps/admin/src/app/api/users/route.ts b/apps/admin/src/app/api/users/route.ts index a4a2662..298189f 100644 --- a/apps/admin/src/app/api/users/route.ts +++ b/apps/admin/src/app/api/users/route.ts @@ -414,14 +414,15 @@ export async function PUT(request: NextRequest) { return forbiddenResponse(`Not authorized to assign role '${role}'`); } - // Authorization: trainers cannot reassign users to different gyms + // Authorization: trainers and admins cannot reassign users to different gyms + // Only superAdmins can reassign gyms if ( - requesterRole === "trainer" && + requesterRole !== "superAdmin" && gymId !== undefined && gymId !== existingUser.gymId ) { return forbiddenResponse( - "Trainers cannot reassign users to different gyms", + "Only superAdmins can reassign users to different gyms", ); } diff --git a/apps/admin/src/components/users/UserManagement.tsx b/apps/admin/src/components/users/UserManagement.tsx index a9c2a32..cd1b983 100644 --- a/apps/admin/src/components/users/UserManagement.tsx +++ b/apps/admin/src/components/users/UserManagement.tsx @@ -362,8 +362,12 @@ export function UserManagement({ gymId }: UserManagementProps) { /> - {/* Only superAdmins and admins can reassign gyms */} - {user?.publicMetadata?.role !== "trainer" && ( + {/* Only superAdmins can reassign gyms */} + {(() => { + const currentRole = user?.publicMetadata?.role; + console.log("Current user role for gym selector:", currentRole); + return currentRole === "superAdmin"; + })() && (