fixed handle error connect keycloak
This commit is contained in:
parent
071140d98a
commit
b5fb2346ab
1 changed files with 147 additions and 54 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue