203 lines
4.9 KiB
TypeScript
203 lines
4.9 KiB
TypeScript
import { clerkClient } from "@clerk/nextjs/server";
|
|
import log from "./logger";
|
|
|
|
/**
|
|
* User roles available in the application
|
|
*/
|
|
export type UserRole = "admin" | "trainer" | "client";
|
|
|
|
/**
|
|
* Set a user's role in Clerk public metadata
|
|
* This will trigger a webhook that syncs the role to the database
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @param role - Role to assign (admin, trainer, or client)
|
|
* @returns Updated user object
|
|
*
|
|
* @example
|
|
* await setUserRole('user_abc123', 'admin');
|
|
*/
|
|
export async function setUserRole(userId: string, role: UserRole) {
|
|
const client = await clerkClient();
|
|
|
|
return await client.users.updateUser(userId, {
|
|
publicMetadata: { role },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a user's role from Clerk public metadata
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @returns User role or null if not set
|
|
*
|
|
* @example
|
|
* const role = await getUserRole('user_abc123');
|
|
* console.log(role); // 'admin' | 'trainer' | 'client' | null
|
|
*/
|
|
export async function getUserRole(userId: string): Promise<UserRole | null> {
|
|
const client = await clerkClient();
|
|
const user = await client.users.getUser(userId);
|
|
|
|
const role = user.publicMetadata?.role as UserRole | undefined;
|
|
return role || null;
|
|
}
|
|
|
|
/**
|
|
* Check if a user has a specific role
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @param role - Role to check
|
|
* @returns True if user has the role
|
|
*
|
|
* @example
|
|
* const isAdmin = await hasRole('user_abc123', 'admin');
|
|
*/
|
|
export async function hasRole(
|
|
userId: string,
|
|
role: UserRole,
|
|
): Promise<boolean> {
|
|
const userRole = await getUserRole(userId);
|
|
return userRole === role;
|
|
}
|
|
|
|
/**
|
|
* Check if a user is an admin
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @returns True if user is an admin
|
|
*
|
|
* @example
|
|
* const isAdmin = await isAdmin('user_abc123');
|
|
*/
|
|
export async function isAdmin(userId: string): Promise<boolean> {
|
|
return hasRole(userId, "admin");
|
|
}
|
|
|
|
/**
|
|
* Check if a user is a trainer
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @returns True if user is a trainer
|
|
*
|
|
* @example
|
|
* const isTrainer = await isTrainer('user_abc123');
|
|
*/
|
|
export async function isTrainer(userId: string): Promise<boolean> {
|
|
return hasRole(userId, "trainer");
|
|
}
|
|
|
|
/**
|
|
* Check if a user is a client
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @returns True if user is a client
|
|
*
|
|
* @example
|
|
* const isClient = await isClient('user_abc123');
|
|
*/
|
|
export async function isClient(userId: string): Promise<boolean> {
|
|
return hasRole(userId, "client");
|
|
}
|
|
|
|
/**
|
|
* Bulk update roles for multiple users
|
|
*
|
|
* @param userRoles - Array of {userId, role} objects
|
|
* @returns Array of updated users
|
|
*
|
|
* @example
|
|
* await bulkSetUserRoles([
|
|
* { userId: 'user_abc123', role: 'admin' },
|
|
* { userId: 'user_def456', role: 'trainer' },
|
|
* ]);
|
|
*/
|
|
export async function bulkSetUserRoles(
|
|
userRoles: Array<{ userId: string; role: UserRole }>,
|
|
): Promise<void> {
|
|
const client = await clerkClient();
|
|
|
|
await Promise.all(
|
|
userRoles.map(({ userId, role }) =>
|
|
client.users.updateUser(userId, {
|
|
publicMetadata: { role },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all users with a specific role
|
|
*
|
|
* @param role - Role to filter by
|
|
* @returns Array of users with the specified role
|
|
*
|
|
* @example
|
|
* const admins = await getUsersByRole('admin');
|
|
*/
|
|
export async function getUsersByRole(role: UserRole) {
|
|
const client = await clerkClient();
|
|
|
|
// Clerk doesn't support filtering by metadata directly, so we fetch all users
|
|
// and filter client-side. For large user bases, consider caching or database queries.
|
|
const { data: users } = await client.users.getUserList();
|
|
|
|
return users.filter(
|
|
(user) => (user.publicMetadata?.role as UserRole) === role,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get user count by role
|
|
*
|
|
* @returns Object with counts for each role
|
|
*
|
|
* @example
|
|
* const counts = await getUserCountByRole();
|
|
* console.log(counts); // { admin: 2, trainer: 5, client: 100 }
|
|
*/
|
|
export async function getUserCountByRole(): Promise<Record<UserRole, number>> {
|
|
const client = await clerkClient();
|
|
const { data: users } = await client.users.getUserList();
|
|
|
|
const counts: Record<UserRole, number> = {
|
|
admin: 0,
|
|
trainer: 0,
|
|
client: 0,
|
|
};
|
|
|
|
users.forEach((user) => {
|
|
const role = (user.publicMetadata?.role as UserRole) || "client";
|
|
counts[role]++;
|
|
});
|
|
|
|
return counts;
|
|
}
|
|
|
|
/**
|
|
* Sync user role between Clerk and database manually
|
|
* Useful for fixing inconsistencies or after manual changes
|
|
*
|
|
* @param userId - Clerk user ID
|
|
* @returns Success status
|
|
*
|
|
* @example
|
|
* await syncUserRole('user_abc123');
|
|
*/
|
|
export async function syncUserRole(userId: string): Promise<boolean> {
|
|
try {
|
|
const client = await clerkClient();
|
|
const user = await client.users.getUser(userId);
|
|
|
|
// Trigger a webhook by updating the user (even with same data)
|
|
await client.users.updateUser(userId, {
|
|
publicMetadata: user.publicMetadata,
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
log.error("Failed to sync user role", error, { userId });
|
|
return false;
|
|
}
|
|
}
|