From de1cd96ec212674d9f6a899f9c641a796df4d4ff Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 21 Apr 2026 12:37:21 -0700 Subject: [PATCH] fix(auth): classify services by service_type, not name heuristics Coolify exposes the template slug on `service_type`; the list endpoint returns only summaries, so the auth list handler now fetches each service individually to classify it reliably. Users can name auth services anything (e.g. "my-login") and they still show up as auth providers. Made-with: Cursor --- app/api/workspaces/[slug]/auth/route.ts | 28 +++++++++++++++---------- lib/coolify.ts | 2 ++ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/api/workspaces/[slug]/auth/route.ts b/app/api/workspaces/[slug]/auth/route.ts index fdf47c4..62a0b5b 100644 --- a/app/api/workspaces/[slug]/auth/route.ts +++ b/app/api/workspaces/[slug]/auth/route.ts @@ -70,18 +70,23 @@ export async function GET( try { const all = await listServicesInProject(ws.coolify_project_uuid); - // Coolify returns all services — we narrow to ones whose name - // contains an auth-provider slug. We also return the full list so - // callers can see non-auth services without a separate endpoint. + // Coolify's list endpoint only returns summaries (no service_type) so + // we fetch each service individually to classify it by template slug. + // This is O(n) in services-per-workspace — acceptable at single-digit + // scales — and avoids name-based heuristics that break on custom names. + const detailed = await Promise.all(all.map(s => getService(s.uuid).catch(() => s))); return NextResponse.json({ workspace: { slug: ws.slug, coolifyProjectUuid: ws.coolify_project_uuid }, - providers: all - .filter(s => AUTH_PROVIDER_SLUGS.has(deriveTypeFromName(s.name))) + providers: detailed + .filter(s => { + const t = resolveProviderSlug(s); + return !!t && AUTH_PROVIDER_SLUGS.has(t); + }) .map(s => ({ uuid: s.uuid, name: s.name, status: s.status ?? null, - provider: deriveTypeFromName(s.name), + provider: resolveProviderSlug(s), projectUuid: projectUuidOf(s), })), allowedProviders: Object.keys(AUTH_PROVIDERS), @@ -159,14 +164,15 @@ export async function POST( } /** - * Coolify names services "{type}-{random-suffix}" when auto-named. We - * recover the provider slug by stripping the trailing `-\w+` if any - * and matching against our allowlist. Falls back to empty string. + * Authoritative: Coolify stores the template slug on `service_type`. + * Fall back to a name-prefix match so services created before that field + * existed still classify correctly. */ -function deriveTypeFromName(name: string): string { +function resolveProviderSlug(svc: { name: string; service_type?: string }): string { + if (svc.service_type && AUTH_PROVIDER_SLUGS.has(svc.service_type)) return svc.service_type; const candidates = Object.values(AUTH_PROVIDERS).sort((a, b) => b.length - a.length); for (const slug of candidates) { - if (name === slug || name.startsWith(`${slug}-`) || name.startsWith(`${slug}_`)) { + if (svc.name === slug || svc.name.startsWith(`${slug}-`) || svc.name.startsWith(`${slug}_`)) { return slug; } } diff --git a/lib/coolify.ts b/lib/coolify.ts index ad0a979..39b4d7e 100644 --- a/lib/coolify.ts +++ b/lib/coolify.ts @@ -560,6 +560,8 @@ export interface CoolifyService { uuid: string; name: string; status?: string; + /** Coolify template slug the service was provisioned from, e.g. "pocketbase". */ + service_type?: string; project_uuid?: string; environment_id?: number; environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } };