import { DecodedJwt, createDecoder } from "fast-jwt"; const KC_URL = process.env.KC_URL; const KC_REALM = process.env.KC_REALM; const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID; const KC_SECRET = process.env.KC_SERVICE_ACCOUNT_SECRET; console.log(process.env.KC_URL); 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 */ export async function getToken() { if (!KC_CLIENT_ID || !KC_SECRET) { throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature."); } 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"); const res = await fetch(`${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token`, { method: "POST", body: body, }).catch((e) => console.error(e)); if (!res) { throw new Error("Cannot get token from keycloak."); } const data = (await res.json()) as any; if (data && data.access_token) { token = data.access_token; } return token; } /** * 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) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "POST", body: JSON.stringify({ enabled: true, credentials: [{ type: "password", value: password }], 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; } /** * 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) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { // 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(); } /** * 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_REALM}/users`.concat(!!search ? `?search=${search}` : ""), `${KC_URL}/admin/realms/${KC_REALM}/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, })); } export async function getUserCount(first = "", max = "", search = "") { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALM}/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(); } /** * 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 res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify({ enabled: true, credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, ...rest, }), }).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; } /** * 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, // opts: Record, ) { // const { password, ...rest } = opts; const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "content-type": `application/json`, }, method: "PUT", body: JSON.stringify({ enabled: true, // credentials: (password && [{ type: "password", value: opts?.password }]) || undefined, // ...rest, firstName, lastName, }), }).catch((e) => console.log("Keycloak Error: ", e)); console.log("firstName: ", firstName); console.log("lastName: ", lastName); 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; } /** * 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) { const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}`, { // prettier-ignore headers: { "authorization": `Bearer ${await getToken()}`, "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_REALM}/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) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALM}/roles`.concat((name && `/${name}`) || ""), { // 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, 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_REALM}/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 }[]) { const res = await fetch( `${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/role-mappings/realm`, { // prettier-ignore headers: { "authorization": `Bearer ${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_REALM}/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_REALM}/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_REALM}/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_REALM}/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_REALM}/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_REALM}/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_REALM}/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_REALM}/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; }