567 lines
14 KiB
TypeScript
567 lines
14 KiB
TypeScript
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<string, any>) {
|
|
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(search = "") {
|
|
const res = await fetch(
|
|
`${KC_URL}/admin/realms/${KC_REALM}/users`.concat(!!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,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* 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_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;
|
|
}
|
|
|
|
/**
|
|
* 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<string, string>) => ({ id: v.id, name: v.name }));
|
|
}
|
|
|
|
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<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 }[]) {
|
|
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<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_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<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_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;
|
|
}
|