hrms-api-org/src/keycloak/index.ts

898 lines
24 KiB
TypeScript

import { DecodedJwt, createDecoder } from "fast-jwt";
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
*/
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_REALMS}/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;
}
console.log(`token: ${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<string, any>,
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;
}
/**
* 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 res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || 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 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 res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || 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_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<string, string>) => ({
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<string, string>) => ({
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<string, any>) {
const { password, ...rest } = opts;
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({
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<string, any>,
) {
// const { password, ...rest } = opts;
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({
enabled: true,
// credentials: (password && [{ type: "password", value: opts?.password }]) || undefined,
// ...rest,
firstName,
lastName,
}),
}).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 res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${token || 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_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<string, string>) => ({
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<string, string>) => ({ 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<string, string>) => ({ 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<string, string>) => ({ 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 res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"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));
return true;
} catch (error) {
console.error("Error changing password:", error);
return false;
}
}
/**
* Update user attributes in Keycloak
*
* @param userId - Keycloak user ID
* @param attributes - Object containing attribute names and their values (as arrays)
* @returns true if success, false otherwise
*/
export async function updateUserAttributes(
userId: string,
attributes: Record<string, string[]>,
): Promise<boolean> {
try {
// Get existing user data to preserve other attributes
const existingUser = await getUser(userId);
if (!existingUser) {
console.error(`User ${userId} not found in Keycloak`);
return false;
}
// Merge existing attributes with new attributes
// Keycloak requires id to be present in the payload
const updatedAttributes = {
id: existingUser.id,
enabled: existingUser.enabled ?? true,
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 ${await getToken()}`,
"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;
}
}
// Function to reset password
export async function resetPassword(username: string) {
try {
// if (!API_KEY || !AUTH_ACCOUNT_SECRET) {
// throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
// }
// const body = new URLSearchParams();
// body.append("client_id", "gettoken");
// body.append("client_secret", AUTH_ACCOUNT_SECRET?.toString());
// body.append("grant_type", "client_credentials");
// const tokenResponse = await fetch(`${process.env.KC_URL}/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, {
// method: "POST",
// headers: {
// "Content-Type": "application/x-www-form-urlencoded",
// api_key: API_KEY,
// },
// body: body
// });
// if (!tokenResponse.ok) {
// throw new Error("Failed to get admin token");
// }
// const tokenData = await tokenResponse.json();
// const adminToken = tokenData.access_token;
const users = await fetch(
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
{
headers: {
authorization: `Bearer ${await getToken()}`,
// "authorization": `Bearer ${adminToken}`,
"content-type": `application/json`,
},
},
);
if (!users.ok) {
return false;
}
const usersData = await users.json();
const userId = usersData[0].id;
const resetResponse = await fetch(
`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${await getToken()}`,
// "Authorization": `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(["UPDATE_PASSWORD"]),
},
);
if (!resetResponse.ok) {
return false;
}
return { message: "Password reset email sent" };
} catch (error) {
console.error("Error triggering password reset:", error);
return false;
}
}