editia-core
Version:
Core services and utilities for Editia applications - Authentication, Monetization, Video Generation Types, and Database Management
224 lines • 8.97 kB
JavaScript
"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