import type { Provider } from '@supabase/supabase-js'; import authConfig from './auth.config'; type SupabaseExternalProvider = Provider | 'email'; interface SupabaseAuthSettings { external: Record; disable_signup: boolean; mailer_autoconfirm: boolean; } export class AuthProvidersService { private supabaseUrl: string; private cache: Map< string, { data: SupabaseAuthSettings; timestamp: number } > = new Map(); private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes constructor(supabaseUrl: string) { this.supabaseUrl = supabaseUrl; } async fetchAuthSettings(): Promise { try { const cacheKey = 'auth-settings'; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { return cached.data; } const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!anonKey) { throw new Error('NEXT_PUBLIC_SUPABASE_ANON_KEY is required'); } const response = await fetch( `${this.supabaseUrl}/auth/v1/settings?apikey=${anonKey}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }, ); if (!response.ok) { console.warn( 'Failed to fetch auth settings from Supabase:', response.status, ); return null; } const settings: SupabaseAuthSettings = await response.json(); this.cache.set(cacheKey, { data: settings, timestamp: Date.now() }); return settings; } catch (error) { console.warn('Error fetching auth settings from Supabase:', error); return null; } } isPasswordEnabled({ settings, }: { settings: SupabaseAuthSettings | null; }): boolean { if (settings) { return settings.external.email === true && !settings.disable_signup; } return process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true'; } isMailerAutoconfirmEnabled({ settings, }: { settings: SupabaseAuthSettings | null; }): boolean { return settings?.mailer_autoconfirm === true; } isMagicLinkEnabled(): boolean { return process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true'; } isOAuthProviderEnabled({ provider, settings, }: { provider: SupabaseExternalProvider; settings: SupabaseAuthSettings | null; }): boolean { if (settings && settings.external) { return settings.external[provider] === true; } return false; } getEnabledOAuthProviders({ settings, }: { settings: SupabaseAuthSettings | null; }): SupabaseExternalProvider[] { const enabledProviders: SupabaseExternalProvider[] = []; if (settings && settings.external) { for (const [providerName, isEnabled] of Object.entries( settings.external, )) { if (isEnabled && providerName !== 'email') { enabledProviders.push(providerName as SupabaseExternalProvider); } } return enabledProviders; } const potentialProviders: SupabaseExternalProvider[] = ['keycloak']; const enabledFallback: SupabaseExternalProvider[] = []; for (const provider of potentialProviders) { if ( provider !== 'email' && this.isOAuthProviderEnabled({ provider, settings }) ) { enabledFallback.push(provider); } } return enabledFallback; } async getAuthConfig() { const settings = await this.fetchAuthSettings(); const [ passwordEnabled, magicLinkEnabled, oAuthProviders, isMailerAutoconfirmEnabled, ] = await Promise.all([ this.isPasswordEnabled({ settings }), this.isMagicLinkEnabled(), this.getEnabledOAuthProviders({ settings }), this.isMailerAutoconfirmEnabled({ settings }), ]); return { providers: { password: passwordEnabled, magicLink: magicLinkEnabled, oAuth: oAuthProviders, }, displayTermsCheckbox: authConfig.displayTermsCheckbox, isMailerAutoconfirmEnabled, }; } clearCache(): void { this.cache.clear(); } } export function createAuthProvidersService(): AuthProvidersService { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; if (!supabaseUrl) { throw new Error('NEXT_PUBLIC_SUPABASE_URL is required'); } return new AuthProvidersService(supabaseUrl); }