import { DecodedJwt, createDecoder } from "fast-jwt"; /** * RateLimiter * Limits the rate of API calls to avoid overwhelming the server */ export class RateLimiter { private requestsPerSecond: number; private requestTimes: number[] = []; constructor(requestsPerSecond: number = 10) { this.requestsPerSecond = requestsPerSecond; } /** * Throttle requests to stay within rate limit * Waits if rate limit would be exceeded */ async throttle(): Promise { const now = Date.now(); // Remove timestamps older than 1 second this.requestTimes = this.requestTimes.filter((t) => now - t < 1000); if (this.requestTimes.length >= this.requestsPerSecond) { const oldestRequest = this.requestTimes[0]; const waitTime = 1000 - (now - oldestRequest); if (waitTime > 0) { await new Promise((resolve) => setTimeout(resolve, waitTime)); } } this.requestTimes.push(Date.now()); } /** * Reset the rate limiter (e.g., after a long pause) */ reset(): void { this.requestTimes = []; } } /** * Check if an error is a network error (retryable) * @param error - Error to check * @returns true if error is network-related and retryable */ function isNetworkError(error: any): boolean { if (!error) return false; // Check for fetch network errors if (error.name === "TypeError" && error.message.includes("fetch")) { return true; } // Check for ECONNREFUSED, ETIMEDOUT, etc. if (error.code && ["ECONNREFUSED", "ETIMEDOUT", "ECONNRESET", "ENOTFOUND"].includes(error.code)) { return true; } return false; } /** * Check if an HTTP status code is retryable * @param status - HTTP status code * @returns true if status code indicates a temporary error */ function isRetryableStatus(status: number): boolean { // Retry on 5xx errors (server errors) and 429 (rate limit) return status >= 500 || status === 429; } /** * Retry wrapper with exponential backoff * Retries failed operations with increasing delay between attempts * * @param fn - Function to execute * @param maxRetries - Maximum number of retry attempts * @param baseDelay - Base delay in milliseconds (doubles each retry) * @returns Promise with result of fn */ export async function withRetry( fn: () => Promise, maxRetries: number = 3, baseDelay: number = 1000, ): Promise { let lastError: any; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error: any) { lastError = error; // Check if error is retryable const isRetryable = isNetworkError(error) || isRetryableStatus(error?.status); if (!isRetryable) { // Don't retry on permanent errors (4xx except 429) throw error; } if (attempt < maxRetries) { // Calculate delay with exponential backoff const delay = baseDelay * Math.pow(2, attempt); console.log( `[withRetry] Attempt ${attempt + 1}/${maxRetries + 1} failed, retrying in ${delay}ms...`, error.message || error, ); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError; } /** * Fetch with timeout * Aborts request if it takes longer than specified timeout */ async function fetchWithTimeout( url: RequestInfo | URL, options: RequestInit = {}, timeout: number = 10000, ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timeoutId); return response; } catch (error: any) { clearTimeout(timeoutId); if (error.name === "AbortError") { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } } const KC_URL = process.env.KC_URL; const KC_REALMS = process.env.KC_REALMS; const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; const AUTH_ACCOUNT_SECRET = process.env.AUTH_ACCOUNT_SECRET; const API_KEY = process.env.API_KEY; let token: string | null = null; let decoded: DecodedJwt | null = null; const jwtDecode = createDecoder({ complete: true }); /** * Check if token is expired or will expire in 30 seconds * @returns true if expire or can't get exp, false otherwise */ export function isTokenExpired(token: string, beforeExpire: number = 30) { decoded = jwtDecode(token); if (decoded && decoded.payload.exp) { return Date.now() / 1000 >= decoded.payload.exp - beforeExpire; } return true; } /** * Get token from keycloak if needed * Returns null if Keycloak is unavailable */ export async function getToken(): Promise { if (!KC_CLIENT_ID || !KC_SECRET) { console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required"); return null; } if (token && !isTokenExpired(token)) return token; const body = new URLSearchParams(); body.append("client_id", KC_CLIENT_ID); body.append("client_secret", KC_SECRET); body.append("grant_type", "client_credentials"); try { const res = await fetchWithTimeout( `${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, { method: "POST", body: body, }, 10000, ); if (!res.ok) { console.error(`[getToken] Keycloak token request failed: ${res.status}`); return null; } const data = (await res.json()) as any; if (data && data.access_token) { token = data.access_token; console.log(`[getToken] Token refreshed successfully`); return token; } console.error("[getToken] No access_token in response"); return null; } catch (error: any) { console.error(`[getToken] Failed to get token: ${error.message}`); return null; } } /** * Create keycloak user by given username and password with roles * * Client must have permission to manage realm's user * * @returns user uuid or true if success, false otherwise. */ export async function createUser( username: string, password: string, opts?: Record, token?: string, ) { const authToken = token || (await getToken()); if (!authToken) { console.error("[createUser] Failed to get Keycloak token"); return false; } const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { // prettier-ignore headers: { "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, method: "POST", body: JSON.stringify({ enabled: true, credentials: [{ type: "password", value: password, temporary: false }], username, ...opts, }), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return await res.json(); } const path = res.headers.get("Location"); const id = path?.split("/").at(-1); return id || true; } /** * Get keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user if success, false otherwise. */ export async function getUser(userId: string, token?: string) { const authToken = token || (await getToken()); if (!authToken) { console.error("[getUser] Failed to get Keycloak token"); return false; } const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return await res.json(); } /** * Get keycloak user by Username (citizenId) * * Client must have permission to manage realm's user * * @returns user if success, false otherwise. */ export async function getUserByUsername(citizenId: string, token?: string) { const authToken = token || (await getToken()); if (!authToken) { console.error("[getUserByUsername] Failed to get Keycloak token"); return false; } const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, { // prettier-ignore headers: { "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return await res.json(); } /** * Get keycloak user list * * Client must have permission to manage realm's user * * @returns user list if success, false otherwise. */ export async function getUserList(first = "", max = "", search = "") { const res = await fetch( // `${KC_URL}/admin/realms/${KC_REALMS}/users`.concat(!!search ? `?search=${search}` : ""), `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first || "0"}&max=${max || "-1"}${search ? `&search=${search}` : ""}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return ((await res.json()) as any[]).map((v: Record) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, enabled: v.enabled, })); } export async function getUserCount(first = "", max = "", search = "") { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/count?first=${first || "0"}&max=${max || "-1"}${search ? `&search=${search}` : ""}`, { headers: { authorization: `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return await res.json(); } /** * Get keycloak user list * * Client must have permission to manage realm's user * * @returns user list if success, false otherwise. */ export async function getUserListOrg(first = "", max = "", search = "", userIds: string[] = []) { const userIdsParam = userIds.join(","); const res = await fetch( // `${KC_URL}/admin/realms/${KC_REALMS}/users`.concat(!!search ? `?search=${search}` : ""), `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first || "0"}&max=${max || "-1"}${search ? `&search=${search}` : ""}${userIdsParam ? `&id=${userIdsParam}` : ""}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return ((await res.json()) as any[]).map((v: Record) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, enabled: v.enabled, })); } export async function getUserCountOrg(first = "", max = "", search = "", userIds: string[] = []) { const userIdsParam = userIds.join(","); const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/count?first=${first || "0"}&max=${max || "-1"}${search ? `&search=${search}` : ""}${userIdsParam && userIdsParam != "" ? `&id=${userIdsParam}` : ""}`, { headers: { authorization: `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return await res.json(); } /** * Update keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user uuid or true if success, false otherwise. */ export async function editUser(userId: string, opts: Record) { const { password, ...rest } = opts; const token = await getToken(); if (!token) { console.error("[editUser] Failed to get Keycloak token"); return false; } // Get existing user data to preserve other fields const existingUser = await getUser(userId, token); if (!existingUser) { console.error(`[editUser] User ${userId} not found in Keycloak`); return false; } // Merge existing user data with updated fields const updatedUser = { ...existingUser, ...rest, credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, }; const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${token}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify(updatedUser), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return await res.json(); } const path = res.headers.get("Location"); const id = path?.split("/").at(-1); return id || true; } /** * Update keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user uuid or true if success, false otherwise. */ export async function updateName( userId: string, firstName: string, lastName: string, prefix: string, // opts: Record, ) { // const { password, ...rest } = opts; // Get existing user data to preserve other fields const existingUser = await getUser(userId); if (!existingUser) { console.error(`[updateName] User ${userId} not found in Keycloak`); return false; } // Merge existing user data with updated name fields const updatedUser = { ...existingUser, firstName, lastName, attributes: { ...(existingUser.attributes || {}), prefix, }, }; const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify(updatedUser), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } const path = res.headers.get("Location"); const id = path?.split("/").at(-1); return id || true; } /** * enable keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user uuid or true if success, false otherwise. */ export async function enableStatus(userId: string, status: boolean) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { headers: { authorization: `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify({ enabled: status, }), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return await res.json(); } const path = res.headers.get("Location"); const id = path?.split("/").at(-1); return id || true; } /** * Delete keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user true if success, false otherwise. */ export async function deleteUser(userId: string, token?: string) { const authToken = token || (await getToken()); if (!authToken) { console.error("[deleteUser] Failed to get Keycloak token"); return false; } const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${authToken}`, "content-type": `application/json`, }, method: "DELETE", }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Get keycloak user by uuid * * Client must have permission to manage realm's user * * @returns user if success, false otherwise. */ export async function getRoleMappings(userId: string) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/role-mappings/realm`, { headers: { authorization: `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return await res.json(); } /** * Get roles list or specific role data * * Client must have permission to get realms roles * * @returns role's info (array if not specify name) if success, null if not found, false otherwise. */ export async function getRoles(name?: string, token?: string) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/roles`.concat((name && `/${name}`) || ""), { // prettier-ignore headers: { "authorization": `Bearer ${token || await getToken()}`, }, }, ).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } if (res.status === 404) { return null; } const data = (await res.json()) as any; if (Array.isArray(data)) { return data.map((v: Record) => ({ id: v.id, name: v.name, description: v.description, })); } // return { // id: data.id, // name: data.name, // }; } /** * Get roles list of user * * Client must have permission to get realms roles * * @returns role's info (array if not specify name) if success, null if not found, false otherwise. */ export async function getUserRoles(userId: string) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/role-mappings/realm`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, }, ).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } if (res.status === 404) { return null; } const data = (await res.json()) as any; if (Array.isArray(data)) { return data.map((v: Record) => ({ id: v.id, name: v.name })); } return { id: data.id, name: data.name, }; } /** * Assign role to user * * Client must have permission to manage realm's user roles * * @returns true if success, false otherwise. */ export async function addUserRoles( userId: string, roles: { id: string; name: string }[], token?: string, ) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/role-mappings/realm`, { // prettier-ignore headers: { "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, method: "POST", body: JSON.stringify(roles), }, ).catch((e) => console.log(e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Remove role from user * * Client must have permission to manage realm's user roles * * @returns true if success, false otherwise. */ export async function removeUserRoles(userId: string, roles: { id: string; name: string }[]) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/role-mappings/realm`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "DELETE", body: JSON.stringify(roles), }, ).catch((e) => console.log(e)); if (!res) return false; if (!res.ok) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Get group list or specific group data * * Client must have permission to manage realms group * * @returns group's info (array if not specify name) if success, null if not found, false otherwise. */ export async function getGroups(id?: string) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/groups`.concat((id && `/${id}`) || ""), { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, }, ).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } if (res.status === 404) { return null; } const data = (await res.json()) as any; if (Array.isArray(data)) { return data.map((v: Record) => ({ id: v.id, name: v.name })); } return { id: data.id, name: data.name, }; } /** * Create group * * Client must have permission to manage realms group * * @returns true if success, false otherwise. */ export async function createGroup(name: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/groups`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "POST", body: JSON.stringify({ name }), }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Edit group * * Client must have permission to manage realms group * * @returns true if success, false otherwise. */ export async function editGroup(id: string, name: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/groups/${id}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify({ name }), }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Delete group * * Client must have permission to manage realms group * * @returns true if success, false otherwise. */ export async function deleteGroup(id: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/groups/${id}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, method: "DELETE", }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Get group list or specific group data * * Client must have permission to manage realms group * * @returns group's info (array if not specify name) if success, null if not found, false otherwise. */ export async function getUserGroups(userId?: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/groups`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } if (res.status === 404) { return null; } const data = (await res.json()) as any; if (Array.isArray(data)) { return data.map((v: Record) => ({ id: v.id, name: v.name })); } return { id: data.id, name: data.name, }; } /** * Add group to user * * Client must have permission to manage user group * * @returns true if success, false otherwise. */ export async function addUserGroup(userId: string, groupId: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/groups/${groupId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, method: "PUT", }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } /** * Delete group from user * * Client must have permission to manage user group * * @returns true if success, false otherwise. */ export async function removeUserGroup(userId: string, groupId: string) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/groups/${groupId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, }, method: "PUT", }).catch((e) => console.log(e)); if (!res) return false; if (!res.ok && res.status !== 404) { return Boolean(console.error("Keycloak Error Response: ", await res.json())); } return true; } // Function to change user password export async function changeUserPassword(userId: string, newPassword: string) { try { const token = await getToken(); if (!token) { console.error("[changeUserPassword] Failed to get Keycloak token"); return false; } const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, { // prettier-ignore headers: { "authorization": `Bearer ${token}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify({ type: "password", value: newPassword, temporary: false, // Set to true if the user must reset their password on next login }), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) { console.error("[changeUserPassword] No response from Keycloak"); return false; } if (!res.ok) { console.error(`[changeUserPassword] Failed to change password: ${res.status}`); return false; } return true; } catch (error) { console.error("Error changing password:", error); return false; } } // Function to reset password export async function resetPassword(username: string) { try { const token = await getToken(); if (!token) { console.error("[resetPassword] Failed to get Keycloak token"); return false; } const users = await fetchWithTimeout( `${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`, { headers: { authorization: `Bearer ${token}`, "content-type": `application/json`, }, }, 10000, ); if (!users.ok) { const errorText = await users.text(); console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`); return false; } const usersData = await users.json(); if (!usersData || usersData.length === 0) { console.error(`[resetPassword] User not found with email: ${username}`); return false; } const userId = usersData[0].id; const resetResponse = await fetchWithTimeout( `${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`, { method: "PUT", headers: { Authorization: `Bearer ${await getToken()}`, "Content-Type": "application/json", }, body: JSON.stringify(["UPDATE_PASSWORD"]), }, 10000, ); if (!resetResponse.ok) { const errorText = await resetResponse.text(); console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`); return false; } console.log(`[resetPassword] Password reset email sent successfully to: ${username}`); return { message: "Password reset email sent" }; } catch (error: any) { console.error(`[resetPassword] Error triggering password reset: ${error.message}`); return false; } } export async function updateUserAttributes( userId: string, attributes: Record, ): Promise { try { const token = await getToken(); if (!token) { console.error("[updateUserAttributes] Failed to get Keycloak token"); return false; } // Get existing user data to preserve other attributes const existingUser = await getUser(userId, token); if (!existingUser) { console.error(`User ${userId} not found in Keycloak`); return false; } // Merge existing attributes with new attributes // IMPORTANT: Spread all existing user fields to preserve firstName, lastName, email, etc. // The Keycloak PUT endpoint performs a full update, so we must include all fields const updatedAttributes = { ...existingUser, attributes: { ...(existingUser.attributes || {}), ...attributes, }, }; console.log( `[updateUserAttributes] Sending to Keycloak:`, JSON.stringify(updatedAttributes, null, 2), ); const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, { headers: { authorization: `Bearer ${token}`, "content-type": "application/json", }, method: "PUT", body: JSON.stringify(updatedAttributes), }).catch((e) => { console.error(`[updateUserAttributes] Network error:`, e); return null; }); if (!res) { console.error(`[updateUserAttributes] No response from Keycloak`); return false; } if (!res.ok) { const errorText = await res.text(); console.error(`[updateUserAttributes] Keycloak Error (${res.status}):`, errorText); return false; } console.log(`[updateUserAttributes] Successfully updated attributes for user ${userId}`); return true; } catch (error) { console.error(`[updateUserAttributes] Error updating attributes for user ${userId}:`, error); return false; } } export async function getAllUsersPaginated( search: string = "", batchSize: number = 100, ): Promise< | Array<{ id: string; username: string; firstName?: string; lastName?: string; email?: string; enabled: boolean; }> | false > { const allUsers: any[] = []; let first = 0; let hasMore = true; while (hasMore) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALMS}/users?first=${first}&max=${batchSize}${search ? `&search=${search}` : ""}`, { headers: { authorization: `Bearer ${await getToken()}`, "content-type": `application/json`, }, }, ).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { const errorText = await res.text(); console.error("Keycloak Error Response: ", errorText); return false; } const rawText = await res.text(); try { const batch = JSON.parse(rawText) as any[]; if (batch.length === 0) { hasMore = false; } else { allUsers.push(...batch); first += batch.length; hasMore = batch.length === batchSize; // Log progress for large datasets if (allUsers.length % 500 === 0) { console.log(`[getAllUsersPaginated] Fetched ${allUsers.length} users so far...`); } } } catch (error) { console.error(`[getAllUsersPaginated] Failed to parse JSON response at offset ${first}:`); console.error( `[getAllUsersPaginated] Response preview (first 500 chars):`, rawText.substring(0, 500), ); console.error( `[getAllUsersPaginated] Response preview (last 200 chars):`, rawText.slice(-200), ); throw new Error(`Failed to parse Keycloak response as JSON at batch starting at ${first}.`); } } console.log(`[getAllUsersPaginated] Total users fetched: ${allUsers.length}`); return allUsers.map((v: any) => ({ id: v.id, username: v.username, firstName: v.firstName, lastName: v.lastName, email: v.email, enabled: v.enabled === true || v.enabled === "true", })); } /** * Create keycloak user by given username and password with roles * * Client must have permission to manage realm's user * * @returns user uuid or true if success, false otherwise. */ export async function createUserHaveProfile( username: string, password: string, profileId: string, prefix: string, opts?: Record, token?: string, ) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, { // prettier-ignore headers: { "authorization": `Bearer ${token || await getToken()}`, "content-type": `application/json`, }, method: "POST", body: JSON.stringify({ enabled: true, credentials: [{ type: "password", value: password, temporary: false }], username, ...opts, }), }).catch((e) => console.log("Keycloak Error: ", e)); if (!res) return false; if (!res.ok) { // return Boolean(console.error("Keycloak Error Response: ", await res.json())); return await res.json(); } const path = res.headers.get("Location"); const id = path?.split("/").at(-1); return id || true; }