chore(services): rename services/provisioning -> services/platform-api
O.0 prep from OPERATOR-PLAN.md. Mechanical refactor before adding partner management and operator-specific endpoints. The service now owns more than just provisioning orchestration (it'll soon own partners, tenant lifecycle actions, multi-audience JWT validation), so the name 'platform-api' reflects its scope better. What changed: - Directory: services/provisioning/ -> services/platform-api/ - Package: @dezky/provisioning -> @dezky/platform-api - Docker: container_name dezky-provisioning -> dezky-platform-api; compose service key 'provisioning' -> 'platform-api'; volume provisioning_node_modules -> platform_api_node_modules - Portal: PROVISIONING_INTERNAL_URL env var -> PLATFORM_API_INTERNAL_URL, default URL http://provisioning:3001 -> http://platform-api:3001 in all three proxy routes (me.get.ts, tenants/index.post.ts, tenants/[slug]/ reconcile.post.ts), plus NUXT_API_BASE updated - Health endpoint service identifier and main.ts log lines updated to 'dezky-platform-api' - Docs swept: README, CLAUDE.md, SERVICES.md, AUTHENTIK-SETUP.md, NEXT-STEPS.md, TROUBLESHOOTING.md, OPERATOR-PLAN.md, traefik/dynamic.yml What deliberately stays: - Internal module names ProvisioningService / ProvisioningModule (those describe an orchestration sub-concern, not the service's purpose) - Tenant.provisioningStatus / provisioningErrors field names (state per integration, not service name) - File services/platform-api/src/tenants/provisioning.service.ts - 'Hetzner provisioning' references in production-prep docs (infrastructure provisioning, unrelated) Verified end-to-end after rename: /api/me returns 200 with profile + 2 tenants + subscription, /api/tenants/dezky/reconcile returns 200 with Authentik integration still ok. OPERATOR-PLAN.md O.0 checkboxes ticked.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
|
||||
interface AuthentikGroup {
|
||||
pk: string
|
||||
name: string
|
||||
attributes?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// 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<string>('AUTHENTIK_API_URL')
|
||||
this.token = config.getOrThrow<string>('AUTHENTIK_API_TOKEN')
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
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<string, unknown> = {}): Promise<AuthentikGroup> {
|
||||
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<AuthentikGroup>('/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<void> {
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user