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;
|
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_URL = process.env.KC_URL;
|
||||||
const KC_REALMS = process.env.KC_REALMS;
|
const KC_REALMS = process.env.KC_REALMS;
|
||||||
const KC_CLIENT_ID = process.env.KC_SERVICE_ACCOUNT_CLIENT_ID;
|
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
|
* 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) {
|
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;
|
if (token && !isTokenExpired(token)) return token;
|
||||||
|
|
@ -158,22 +188,35 @@ export async function getToken() {
|
||||||
body.append("client_secret", KC_SECRET);
|
body.append("client_secret", KC_SECRET);
|
||||||
body.append("grant_type", "client_credentials");
|
body.append("grant_type", "client_credentials");
|
||||||
|
|
||||||
const res = await fetch(`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`, {
|
try {
|
||||||
method: "POST",
|
const res = await fetchWithTimeout(
|
||||||
body: body,
|
`${KC_URL}/realms/${KC_REALMS}/protocol/openid-connect/token`,
|
||||||
}).catch((e) => console.error(e));
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: body,
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
|
||||||
if (!res) {
|
if (!res.ok) {
|
||||||
throw new Error("Cannot get token from keycloak.");
|
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>,
|
opts?: Record<string, any>,
|
||||||
token?: string,
|
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`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${token || await getToken()}`,
|
"authorization": `Bearer ${authToken}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -206,7 +255,6 @@ export async function createUser(
|
||||||
|
|
||||||
if (!res) return false;
|
if (!res) return false;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,10 +271,16 @@ export async function createUser(
|
||||||
* @returns user if success, false otherwise.
|
* @returns user if success, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function getUser(userId: string, token?: string) {
|
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}`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${token || await getToken()}`,
|
"authorization": `Bearer ${authToken}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
}).catch((e) => console.log("Keycloak Error: ", e));
|
}).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.
|
* @returns user if success, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function getUserByUsername(citizenId: string, token?: string) {
|
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}`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users?username=${citizenId}`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${token || await getToken()}`,
|
"authorization": `Bearer ${authToken}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
}).catch((e) => console.log("Keycloak Error: ", e));
|
}).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>) {
|
export async function editUser(userId: string, opts: Record<string, any>) {
|
||||||
const { password, ...rest } = opts;
|
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
|
// Get existing user data to preserve other fields
|
||||||
const existingUser = await getUser(userId);
|
const existingUser = await getUser(userId, token);
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
console.error(`[editUser] User ${userId} not found in Keycloak`);
|
console.error(`[editUser] User ${userId} not found in Keycloak`);
|
||||||
return false;
|
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}`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${await getToken()}`,
|
"authorization": `Bearer ${token}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -405,7 +471,6 @@ export async function editUser(userId: string, opts: Record<string, any>) {
|
||||||
|
|
||||||
if (!res) return false;
|
if (!res) return false;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// return Boolean(console.error("Keycloak Error Response: ", await res.json()));
|
|
||||||
return 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.
|
* @returns user true if success, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function deleteUser(userId: string, token?: string) {
|
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}`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${token || await getToken()}`,
|
"authorization": `Bearer ${authToken}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
@ -890,10 +961,16 @@ export async function removeUserGroup(userId: string, groupId: string) {
|
||||||
// Function to change user password
|
// Function to change user password
|
||||||
export async function changeUserPassword(userId: string, newPassword: string) {
|
export async function changeUserPassword(userId: string, newPassword: string) {
|
||||||
try {
|
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`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/reset-password`, {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": `Bearer ${await getToken()}`,
|
"authorization": `Bearer ${token}`,
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -904,6 +981,15 @@ export async function changeUserPassword(userId: string, newPassword: string) {
|
||||||
}),
|
}),
|
||||||
}).catch((e) => console.log("Keycloak Error: ", e));
|
}).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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error changing password:", error);
|
console.error("Error changing password:", error);
|
||||||
|
|
@ -914,60 +1000,61 @@ export async function changeUserPassword(userId: string, newPassword: string) {
|
||||||
// Function to reset password
|
// Function to reset password
|
||||||
export async function resetPassword(username: string) {
|
export async function resetPassword(username: string) {
|
||||||
try {
|
try {
|
||||||
// if (!API_KEY || !AUTH_ACCOUNT_SECRET) {
|
const token = await getToken();
|
||||||
// throw new Error("KC_CLIENT_ID and KC_SECRET are required to used this feature.");
|
if (!token) {
|
||||||
// }
|
console.error("[resetPassword] Failed to get Keycloak token");
|
||||||
// const body = new URLSearchParams();
|
return false;
|
||||||
// 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(
|
const users = await fetchWithTimeout(
|
||||||
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
|
`${KC_URL}/admin/realms/${KC_REALMS}/users?email=${encodeURIComponent(username)}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${await getToken()}`,
|
authorization: `Bearer ${token}`,
|
||||||
// "authorization": `Bearer ${adminToken}`,
|
|
||||||
"content-type": `application/json`,
|
"content-type": `application/json`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
10000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!users.ok) {
|
if (!users.ok) {
|
||||||
|
const errorText = await users.text();
|
||||||
|
console.error(`[resetPassword] Failed to search user. Status: ${users.status}, Error: ${errorText}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usersData = await users.json();
|
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 userId = usersData[0].id;
|
||||||
const resetResponse = await fetch(
|
|
||||||
|
const resetResponse = await fetchWithTimeout(
|
||||||
`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`,
|
`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}/execute-actions-email`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${await getToken()}`,
|
Authorization: `Bearer ${await getToken()}`,
|
||||||
// "Authorization": `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(["UPDATE_PASSWORD"]),
|
body: JSON.stringify(["UPDATE_PASSWORD"]),
|
||||||
},
|
},
|
||||||
|
10000,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!resetResponse.ok) {
|
if (!resetResponse.ok) {
|
||||||
|
const errorText = await resetResponse.text();
|
||||||
|
console.error(`[resetPassword] Failed to send reset email. Status: ${resetResponse.status}, Error: ${errorText}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[resetPassword] Password reset email sent successfully to: ${username}`);
|
||||||
return { message: "Password reset email sent" };
|
return { message: "Password reset email sent" };
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error triggering password reset:", error);
|
console.error(`[resetPassword] Error triggering password reset: ${error.message}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -977,8 +1064,14 @@ export async function updateUserAttributes(
|
||||||
attributes: Record<string, string[]>,
|
attributes: Record<string, string[]>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
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
|
// Get existing user data to preserve other attributes
|
||||||
const existingUser = await getUser(userId);
|
const existingUser = await getUser(userId, token);
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
console.error(`User ${userId} not found in Keycloak`);
|
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}`, {
|
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALMS}/users/${userId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
authorization: `Bearer ${await getToken()}`,
|
authorization: `Bearer ${token}`,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue