api keycloak setup

This commit is contained in:
tw-blue
2025-11-29 17:58:02 +01:00
parent a9fb52fd4d
commit d6e7de09a4
27 changed files with 548 additions and 68 deletions
@@ -0,0 +1 @@
export * from "./keycloak-admin.js";
@@ -0,0 +1,74 @@
import RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { Injectable } from "../../../infrastructure/di/injectable-decorator";
import kcClientConfig from "./nodejs-api.json" with { type: "json" };
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { Claim } from "../../../models/claim";
import { User } from "../../../models/user";
@Injectable()
export class KeycloakAdmin {
private readonly kcAdminClient: Promise<KeycloakAdminClient>;
constructor() {
this.kcAdminClient = new Promise(async resolve => {
const client = new KeycloakAdminClient({
baseUrl: kcClientConfig.baseUrl,
realmName: "taekwondo",
});
await client.auth({
...kcClientConfig,
grantType: "client_credentials",
});
resolve(client);
});
}
public async addRoleToUser(uid: string, roleName: string): Promise<void> {
const client = await this.kcAdminClient;
const role = await this.getRole(roleName);
if (!role.id || !role.name) {
throw new Error(`Role ${roleName} is missing id or name`);
}
await client.users.addRealmRoleMappings({
id: uid,
roles: [{ id: role.id, name: role.name }],
});
}
public async removeRoleFromUser(uid: string, claim: Claim): Promise<void> {
const client = await this.kcAdminClient;
const role = await this.getRole(claim);
if (!role.id || !role.name) {
throw new Error(`Role ${claim} is missing id or name`);
}
await client.users.delRealmRoleMappings({
id: uid,
roles: [{ id: role.id, name: role.name }],
});
}
public async getUsers(): Promise<User[]> {
const client = await this.kcAdminClient;
const kcUsers = await client.users.find();
return kcUsers.map(kcUser => ({
id: kcUser.id,
username: kcUser.username || "",
roles: (kcUser.realmRoles || []).map(role => role as Claim),
}));
}
private async getRole(name: string): Promise<RoleRepresentation> {
const client = await this.kcAdminClient;
const role = await client.roles.findOneByName({
name: name,
realm: "taekwondo",
});
if (!role) {
throw new Error(`Role ${name} not found`);
}
return role;
}
}
@@ -0,0 +1,60 @@
{
"clientId": "nodejs-api",
"name": "NodejsApi",
"description": "",
"rootUrl": "https://tkd-api.toni714.de",
"adminUrl": "https://tkd-api.toni714.de",
"baseUrl": "https://tkd-api.toni714.de",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp",
"redirectUris": ["https://tkd-api.toni714.de/*"],
"webOrigins": ["https://tkd-api.toni714.de"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"publicClient": false,
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"client.secret.creation.time": "1762463686",
"backchannel.logout.session.required": "true",
"standard.token.exchange.enabled": "true",
"oauth2.device.authorization.grant.enabled": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false",
"dpop.bound.access.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"service_account",
"web-origins",
"acr",
"roles",
"profile",
"basic",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"organization",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
@@ -0,0 +1 @@
export * from "./keycloak-user.js";
@@ -0,0 +1,11 @@
import KeycloakConnect from "keycloak-connect";
import { sessionStore } from "../../../infrastructure/session.js";
import keycloakConfig from "./keycloak.json" with { type: "json" };
export const KC_SECURITY_NAME = "bearerAuth";
export const keycloak = new KeycloakConnect(
{ store: sessionStore },
keycloakConfig,
);
export const kcUserConfig = keycloakConfig;
@@ -0,0 +1,10 @@
{
"realm": "taekwondo",
"auth-server-url": "https://keycloak.toni714.de:8443",
"ssl-required": "external",
"resource": "nodejs-api",
"credentials": {
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp"
},
"confidential-port": 0
}
@@ -0,0 +1,6 @@
import { Injectable } from "../infrastructure/di/index.js";
@Injectable()
export class MembersService {
// public async getMembers(): Promise<Member[]> {}
}
@@ -0,0 +1,8 @@
import { Injectable } from "../infrastructure/di/index.js";
@Injectable()
export class MessagingService {
public async sendTestMessage(): Promise<void> {
throw new Error("Method not implemented.");
}
}
+28
View File
@@ -0,0 +1,28 @@
import { inject } from "../infrastructure/di/index.js";
import { Claim } from "../models/claim";
import { Topic } from "../models/topic.js";
import { User } from "../models/user.js";
import { KeycloakAdmin } from "./keycloak/admin/index.js";
export class UserService {
private readonly keycloakAdmin = inject(KeycloakAdmin);
public async setClaim(
uid: string,
claim: Claim,
value: boolean,
): Promise<void> {
if (value) {
return await this.keycloakAdmin.addRoleToUser(uid, claim);
} else {
return await this.keycloakAdmin.removeRoleFromUser(uid, claim);
}
}
public async getUsers(): Promise<User[]> {
return await this.keycloakAdmin.getUsers();
}
public async setSubscriptions(token: string, topics: Topic[]) {
throw new Error("Method not implemented.");
}
}