UNPKG

editia-core

Version:

Core services and utilities for Editia applications - Authentication, Monetization, Video Generation Types, and Database Management

224 lines 8.97 kB
"use strict"; /** * Unified Clerk Authentication Service * Based on analysis of server-primary and server-analyzer patterns */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ClerkAuthService = void 0; const backend_1 = require("@clerk/backend"); const supabase_js_1 = require("@supabase/supabase-js"); /** * Unified Clerk Authentication Service * Combines the best patterns from server-primary and server-analyzer */ class ClerkAuthService { /** * Initialize the authentication service * Must be called before using any authentication methods */ static initialize(config) { this.config = config; // Initialize Clerk client this.clerkClient = (0, backend_1.createClerkClient)({ secretKey: config.clerkSecretKey, }); // Initialize Supabase client with service role key to bypass RLS this.supabaseClient = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseServiceRoleKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); } /** * Verify user from a Clerk JWT token and return both Clerk user and database user * @param authHeader The Authorization header value containing Clerk JWT * @returns Object containing Clerk user, database user, or error response */ static async verifyUser(authHeader) { // Check if service is initialized if (!this.isInitialized()) { return this.createErrorResponse('Authentication service not initialized', 500); } // Check if auth header exists if (!authHeader) { return this.createErrorResponse('Missing authorization header', 401, 'Include Authorization: Bearer <clerk-jwt-token> in your request'); } // Extract token from header const token = authHeader.replace('Bearer ', ''); // Basic JWT format validation const jwtParts = token.split('.'); if (jwtParts.length !== 3) { return this.createErrorResponse('Invalid JWT format - token should have 3 parts separated by dots', 401); } try { // Verify JWT token with Clerk const verifiedToken = await (0, backend_1.verifyToken)(token, { secretKey: this.config.clerkSecretKey, }); if (!verifiedToken || !verifiedToken.sub) { return this.createErrorResponse('Invalid authentication token', 401); } // Get Clerk user details const clerkUser = await this.clerkClient.users.getUser(verifiedToken.sub); if (!clerkUser) { return this.createErrorResponse('Clerk user not found', 401); } // Get database user using Clerk user ID const { data: databaseUser, error: dbError } = await this.supabaseClient .from('users') .select('*') .eq('clerk_user_id', clerkUser.id) .single(); if (dbError || !databaseUser) { // Try to create the user automatically if they don't exist console.log(`Creating database user for Clerk user: ${clerkUser.id}`); const { data: newUser, error: createError } = await this.supabaseClient .from('users') .insert([ { id: clerkUser.id, // Use Clerk user ID as database user ID clerk_user_id: clerkUser.id, email: clerkUser.emailAddresses[0]?.emailAddress || null, full_name: `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() || null, avatar_url: clerkUser.imageUrl || null, role: 'user' } ]) .select('*') .single(); if (createError || !newUser) { console.error('Failed to create database user:', createError); return { user: null, clerkUser: clerkUser, errorResponse: { success: false, error: 'Database user not found. Please complete onboarding.', status: 404, hint: 'User exists in Clerk but not in database. Complete onboarding process.', }, }; } console.log(`Successfully created database user: ${newUser.id}`); return { user: newUser, clerkUser: clerkUser, errorResponse: null, }; } // Return both Clerk user and database user return { user: databaseUser, clerkUser: clerkUser, errorResponse: null, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return this.createErrorResponse(`Authentication service error: ${errorMessage}`, 500); } } /** * Helper method to get just the database user ID from a Clerk token * @param authHeader The Authorization header value containing Clerk JWT * @returns Database user ID or null */ static async getDatabaseUserId(authHeader) { const { user } = await this.verifyUser(authHeader); return user?.id || null; } /** * Check if user has Pro subscription (required for certain features) * @param authHeader The Authorization header value containing Clerk JWT * @returns True if user has Pro access */ static async hasProAccess(authHeader) { await this.verifyUser(authHeader); // TODO: Implement RevenueCat Pro check // For now, return true to allow testing return true; } /** * Verify user and check Pro access in one call * @param authHeader The Authorization header value containing Clerk JWT * @returns AuthResult with Pro access verification */ static async verifyProUser(authHeader) { const result = await this.verifyUser(authHeader); if (result.errorResponse) { return result; } // Check Pro subscription const hasProAccess = await this.hasProAccess(authHeader); if (!hasProAccess) { return { user: null, clerkUser: null, errorResponse: { success: false, error: 'Pro subscription required for this feature', status: 403, hint: 'Upgrade to Pro plan to access this feature', }, }; } return result; } /** * Delete user from both Clerk and database * @param authHeader The Authorization header value containing Clerk JWT * @returns Success status and error if any */ static async deleteUser(authHeader) { try { const { user, clerkUser } = await this.verifyUser(authHeader); if (!user || !clerkUser) { return { success: false, error: 'User not found' }; } // Delete from database first const { error: dbError } = await this.supabaseClient .from('users') .delete() .eq('clerk_user_id', clerkUser.id); if (dbError) { return { success: false, error: 'Failed to delete user from database' }; } // Delete from Clerk await this.clerkClient.users.deleteUser(clerkUser.id); return { success: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { success: false, error: `Failed to delete user: ${errorMessage}` }; } } /** * Check if the service is properly initialized * @returns True if initialized, false otherwise */ static isInitialized() { return !!(this.clerkClient && this.supabaseClient && this.config); } /** * Create a standardized error response * @param error Error message * @param status HTTP status code * @param hint Optional hint for debugging * @returns AuthErrorResponse */ static createErrorResponse(error, status, hint) { return { user: null, clerkUser: null, errorResponse: { success: false, error, status, hint, }, }; } } exports.ClerkAuthService = ClerkAuthService; //# sourceMappingURL=clerk-auth.js.map