admin role enhancment
basic invitation flow
This commit is contained in:
parent
d112dbb122
commit
b1f01208fa
@ -14,6 +14,10 @@ RESEND_API_KEY=re_your_resend_api_key_here
|
|||||||
EMAIL_FROM=FitAI <noreply@yourdomain.com>
|
EMAIL_FROM=FitAI <noreply@yourdomain.com>
|
||||||
EMAIL_REPLY_TO=support@yourdomain.com
|
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 (optional - defaults to ./fitai.db)
|
||||||
DATABASE_PATH=./fitai.db
|
DATABASE_PATH=./fitai.db
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -84,12 +84,30 @@ export async function POST(request: NextRequest) {
|
|||||||
if (data.sendInvitation && data.email) {
|
if (data.sendInvitation && data.email) {
|
||||||
try {
|
try {
|
||||||
const client = await clerkClient();
|
const client = await clerkClient();
|
||||||
|
|
||||||
|
// Build publicMetadata - only include gymId if it exists
|
||||||
|
const publicMetadata: Record<string, any> = {
|
||||||
|
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({
|
const invitation = await client.invitations.createInvitation({
|
||||||
emailAddress: data.email,
|
emailAddress: data.email,
|
||||||
publicMetadata: {
|
publicMetadata,
|
||||||
role: data.role,
|
redirectUrl,
|
||||||
gymId: assignedGymId,
|
ignoreExisting: true, // Don't fail if invitation already exists
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("Clerk invitation sent", {
|
log.info("Clerk invitation sent", {
|
||||||
@ -97,6 +115,7 @@ export async function POST(request: NextRequest) {
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
gymId: assignedGymId,
|
gymId: assignedGymId,
|
||||||
invitationId: invitation.id,
|
invitationId: invitation.id,
|
||||||
|
redirectUrl: redirectUrl || "default (mobile app)",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send custom invitation email (in addition to Clerk's)
|
// Send custom invitation email (in addition to Clerk's)
|
||||||
@ -117,7 +136,22 @@ export async function POST(request: NextRequest) {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "Unknown error";
|
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}`, {
|
return errorResponse(`Failed to send invitation: ${errorMessage}`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -414,14 +414,15 @@ export async function PUT(request: NextRequest) {
|
|||||||
return forbiddenResponse(`Not authorized to assign role '${role}'`);
|
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 (
|
if (
|
||||||
requesterRole === "trainer" &&
|
requesterRole !== "superAdmin" &&
|
||||||
gymId !== undefined &&
|
gymId !== undefined &&
|
||||||
gymId !== existingUser.gymId
|
gymId !== existingUser.gymId
|
||||||
) {
|
) {
|
||||||
return forbiddenResponse(
|
return forbiddenResponse(
|
||||||
"Trainers cannot reassign users to different gyms",
|
"Only superAdmins can reassign users to different gyms",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -362,8 +362,12 @@ export function UserManagement({ gymId }: UserManagementProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Only superAdmins and admins can reassign gyms */}
|
{/* Only superAdmins can reassign gyms */}
|
||||||
{user?.publicMetadata?.role !== "trainer" && (
|
{(() => {
|
||||||
|
const currentRole = user?.publicMetadata?.role;
|
||||||
|
console.log("Current user role for gym selector:", currentRole);
|
||||||
|
return currentRole === "superAdmin";
|
||||||
|
})() && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium mb-1">Gym</label>
|
<label className="block text-sm font-medium mb-1">Gym</label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@ -16,9 +16,12 @@ export const passwordSchema = z
|
|||||||
export const phoneSchema = z
|
export const phoneSchema = z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.refine((val) => !val || /^\+?[1-9]\d{1,14}$/.test(val), {
|
.refine(
|
||||||
|
(val) => !val || val.trim() === "" || /^\+?[1-9]\d{1,14}$/.test(val),
|
||||||
|
{
|
||||||
message: "Invalid phone number format",
|
message: "Invalid phone number format",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const dateTimeSchema = z.string().datetime("Invalid datetime format");
|
export const dateTimeSchema = z.string().datetime("Invalid datetime format");
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user