fixed handle error connect keycloak

This commit is contained in:
Warunee Tamkoo 2026-04-28 11:05:00 +07:00
parent 071140d98a
commit b5fb2346ab

View file

@ -116,6 +116,34 @@ export async function withRetry<T>(
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<Response> {
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;
@ -144,10 +172,12 @@ export function isTokenExpired(token: string, beforeExpire: number = 30) {
/**
* Get token from keycloak if needed
* Returns null if Keycloak is unavailable
*/
export async function getToken() {
export async function getToken(): Promise<string | null> {
if (!KC_CLIENT_ID || !KC_SECRET) {
throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
console.error("[getToken] KC_CLIENT_ID and KC_SECRET are required");
return null;
}
if (token && !isTokenExpired(token)) return token;
@ -158,22 +188,35 @@ export async function getToken() {
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));
try {
const res = await fetchWithTimeout(
`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`,
{
method: "POST",
body: body,
},
10000,
);
if (!res) {
throw new Error("Cannot get token from keycloak.");
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;
}
const data = (await res.json()) as any;
if (data && data.access_token) {
token = data.access_token;
}
console.log(`token: ${token}`);
return token;
}
/**
@ -189,10 +232,16 @@ export async function createUser(
opts?: Record<string, any>,
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 ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
method: "POST",
@ -206,7 +255,6 @@ export async function createUser(
if (!res) return false;
if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json();
}
@ -223,10 +271,16 @@ export async function createUser(
* @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 ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
}).catch((e) => console.log("Keycloak Error: ", e));
@ -245,10 +299,16 @@ export async function getUser(userId: string, token?: string) {
* @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 ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
}).catch((e) => console.log("Keycloak Error: ", e));
@ -379,8 +439,14 @@ export async function getUserCountOrg(first = "", max = "", search = "", userIds
export async function editUser(userId: string, opts: Record<string, any>) {
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);
const existingUser = await getUser(userId, token);
if (!existingUser) {
console.error(`[editUser] User ${userId} not found in Keycloak`);
return false;
@ -396,7 +462,7 @@ export async function editUser(userId: string, opts: Record<string, any>) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
// prettier-ignore
headers: {
"authorization": `Bearer ${await getToken()}`,
"authorization": `Bearer ${token}`,
"content-type": `application/json`,
},
method: "PUT",
@ -405,7 +471,6 @@ export async function editUser(userId: string, opts: Record<string, any>) {
if (!res) return false;
if (!res.ok) {
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
return await res.json();
}
@ -505,10 +570,16 @@ export async function enableStatus(userId: string, status: boolean) {
* @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 ${token || await getToken()}`,
"authorization": `Bearer ${authToken}`,
"content-type": `application/json`,
},
method: "DELETE",
@ -890,10 +961,16 @@ export async function removeUserGroup(userId: string, groupId: string) {
// 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 ${await getToken()}`,
"authorization": `Bearer ${token}`,
"content-type": `application/json`,
},
method: "PUT",
@ -904,6 +981,15 @@ export async function changeUserPassword(userId: string, newPassword: string) {
}),
}).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);
@ -914,60 +1000,61 @@ export async function changeUserPassword(userId: string, newPassword: string) {
// 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 token = await getToken();
if (!token) {
console.error("[resetPassword] Failed to get Keycloak token");
return false;
}
const users = await fetch(
const users = await fetchWithTimeout(
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
{
headers: {
authorization: `Bearer ${await getToken()}`,
// "authorization": `Bearer ${adminToken}`,
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 fetch(
const resetResponse = await fetchWithTimeout(
`${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"]),
},
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) {
console.error("Error triggering password reset:", error);
} catch (error: any) {
console.error(`[resetPassword] Error triggering password reset: ${error.message}`);
return false;
}
}
@ -977,8 +1064,14 @@ export async function updateUserAttributes(
attributes: Record<string, string[]>,
): Promise<boolean> {
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);
const existingUser = await getUser(userId, token);
if (!existingUser) {
console.error(`User ${userId} not found in Keycloak`);
@ -1003,7 +1096,7 @@ export async function updateUserAttributes(
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
headers: {
authorization: `Bearer ${await getToken()}`,
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
method: "PUT",