import { Injectable, Logger } from '@nestjs/common' import { ConfigService } from '@nestjs/config' interface AuthentikGroup { pk: string name: string attributes?: Record } // Thin wrapper around the Authentik API for the operations the provisioning // service needs. We never expose raw Authentik errors to API callers — they // surface as provisioningErrors.authentik strings. @Injectable() export class AuthentikClient { private readonly logger = new Logger(AuthentikClient.name) private readonly base: string private readonly token: string constructor(config: ConfigService) { this.base = config.getOrThrow('AUTHENTIK_API_URL') this.token = config.getOrThrow('AUTHENTIK_API_TOKEN') } private async request(path: string, init: RequestInit = {}): Promise { const res = await fetch(`${this.base}${path}`, { ...init, headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json', Accept: 'application/json', ...(init.headers ?? {}), }, }) if (!res.ok) { const body = await res.text().catch(() => '') throw new Error(`Authentik ${init.method ?? 'GET'} ${path} → ${res.status}: ${body.slice(0, 200)}`) } return (await res.json()) as T } // Idempotent: returns existing group if name is taken, creates otherwise. async ensureGroup(slug: string, attributes: Record = {}): Promise { const search = await this.request<{ results: AuthentikGroup[] }>( `/core/groups/?name=${encodeURIComponent(slug)}`, ) if (search.results.length > 0) { this.logger.log(`Authentik group "${slug}" already exists (pk=${search.results[0].pk})`) return search.results[0] } const created = await this.request('/core/groups/', { method: 'POST', body: JSON.stringify({ name: slug, attributes: { role: 'tenant', slug, ...attributes }, }), }) this.logger.log(`Created Authentik group "${slug}" (pk=${created.pk})`) return created } async deleteGroup(groupId: string): Promise { const res = await fetch(`${this.base}/core/groups/${groupId}/`, { method: 'DELETE', headers: { Authorization: `Bearer ${this.token}` }, }) if (!res.ok && res.status !== 404) { const body = await res.text().catch(() => '') throw new Error(`Authentik DELETE group ${groupId} → ${res.status}: ${body.slice(0, 200)}`) } this.logger.log(`Deleted Authentik group ${groupId}`) } }