UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

396 lines 16.7 kB
"use strict"; /** * Identity Manager for Epic 1: Universal MakerKit Core System * Manages auth.identities records for complete MakerKit authentication flows * Part of Task 1.1: Implement Auth.Identities Support */ Object.defineProperty(exports, "__esModule", { value: true }); exports.IdentityManager = void 0; const logger_1 = require("../core/utils/logger"); class IdentityManager { constructor(client) { this.client = client; } /** * Create auth.identities record for complete MakerKit auth flow */ async createIdentity(identityData) { const warnings = []; try { logger_1.Logger.debug(`Creating identity for user ${identityData.userId} with provider ${identityData.provider}`); // Validate identity data const validation = this.validateIdentityData(identityData); if (!validation.isValid) { return { success: false, error: `Identity validation failed: ${validation.errors.join(', ')}`, warnings: validation.warnings }; } warnings.push(...validation.warnings); // Check if identity already exists const existingIdentity = await this.findExistingIdentity(identityData.userId, identityData.provider); if (existingIdentity) { logger_1.Logger.debug(`Identity already exists for user ${identityData.userId} with provider ${identityData.provider}`); return { success: true, identity: existingIdentity, warnings: [...warnings, 'Identity already exists - returning existing record'] }; } // Format identity record for auth.identities table const identityRecord = this.formatIdentityRecord(identityData); // Insert identity record const { data: identity, error } = await this.client .from('auth.identities') .insert(identityRecord) .select() .single(); if (error) { // Handle common MakerKit identity constraints if (error.message.includes('duplicate key') || error.message.includes('unique constraint')) { logger_1.Logger.warn(`Identity already exists for ${identityData.provider}:${identityData.providerId}`); const existing = await this.findExistingIdentity(identityData.userId, identityData.provider); return { success: true, identity: existing, warnings: [...warnings, 'Found existing identity record'] }; } throw new Error(`Failed to create identity: ${error.message}`); } logger_1.Logger.success(`✅ Created identity for user ${identityData.userId} with provider ${identityData.provider}`); return { success: true, identity, warnings }; } catch (error) { logger_1.Logger.error(`Identity creation failed for user ${identityData.userId}:`, error); return { success: false, error: error.message, warnings }; } } /** * Create multiple identities for a user (supports multiple OAuth providers) */ async createMultipleIdentities(userId, providers) { logger_1.Logger.debug(`Creating ${providers.length} identities for user ${userId}`); const results = []; for (const provider of providers) { const identityData = { userId, provider: provider.provider, providerId: provider.providerId, providerMetadata: provider.metadata, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const result = await this.createIdentity(identityData); results.push(result); // Short delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 100)); } const successCount = results.filter(r => r.success).length; logger_1.Logger.info(`✅ Created ${successCount}/${providers.length} identities for user ${userId}`); return results; } /** * Generate realistic OAuth provider data for testing */ generateOAuthProviderData(provider, email) { const baseId = email.split('@')[0]; switch (provider) { case 'google': return { provider: 'google', providerId: `google_${baseId}_${Math.random().toString(36).substr(2, 9)}`, metadata: { iss: 'https://accounts.google.com', sub: `google_${baseId}_${Math.random().toString(36).substr(2, 9)}`, email, email_verified: true, name: this.extractNameFromEmail(email), picture: `https://lh3.googleusercontent.com/a/default-user=s96-c`, given_name: this.extractNameFromEmail(email).split(' ')[0], family_name: this.extractNameFromEmail(email).split(' ')[1] || '', locale: 'en' } }; case 'github': return { provider: 'github', providerId: `github_${baseId}_${Math.random().toString(36).substr(2, 9)}`, metadata: { login: baseId, id: Math.floor(Math.random() * 100000000), avatar_url: `https://avatars.githubusercontent.com/u/${Math.floor(Math.random() * 100000000)}`, gravatar_id: '', url: `https://api.github.com/users/${baseId}`, html_url: `https://github.com/${baseId}`, name: this.extractNameFromEmail(email), email, bio: null, public_repos: Math.floor(Math.random() * 50), followers: Math.floor(Math.random() * 1000), following: Math.floor(Math.random() * 500) } }; case 'discord': return { provider: 'discord', providerId: `discord_${Math.random().toString(36).substr(2, 18)}`, metadata: { id: Math.random().toString(36).substr(2, 18), username: baseId, discriminator: String(Math.floor(Math.random() * 9999)).padStart(4, '0'), avatar: `avatar_${Math.random().toString(36).substr(2, 6)}`, email, verified: true, locale: 'en-US', mfa_enabled: Math.random() > 0.7 } }; case 'apple': return { provider: 'apple', providerId: `apple_${baseId}.${Math.random().toString(36).substr(2, 6)}`, metadata: { sub: `${baseId}.${Math.random().toString(36).substr(2, 6)}`, email, email_verified: 'true', is_private_email: 'false', real_user_status: 2 } }; case 'email': default: return { provider: 'email', providerId: email, metadata: { sub: email, email, email_verified: true } }; } } /** * Validate identity data before creation */ validateIdentityData(identityData) { const errors = []; const warnings = []; const recommendations = []; // Required fields validation if (!identityData.userId) { errors.push('userId is required'); } if (!identityData.provider) { errors.push('provider is required'); } if (!identityData.providerId) { errors.push('providerId is required'); } // Provider-specific validation const validProviders = ['email', 'google', 'github', 'discord', 'apple', 'facebook', 'twitter']; if (identityData.provider && !validProviders.includes(identityData.provider)) { errors.push(`Invalid provider: ${identityData.provider}. Valid providers: ${validProviders.join(', ')}`); } // Email provider specific validation if (identityData.provider === 'email') { if (!identityData.email || !identityData.email.includes('@')) { errors.push('Email provider requires valid email address'); } if (identityData.providerId !== identityData.email) { warnings.push('For email provider, providerId should match email address'); } } // OAuth provider validation if (identityData.provider !== 'email' && !identityData.providerMetadata) { warnings.push(`OAuth provider ${identityData.provider} should include provider metadata for realistic testing`); } // UUID validation for userId const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (identityData.userId && !uuidRegex.test(identityData.userId)) { warnings.push('userId should be a valid UUID for MakerKit compatibility'); } // Recommendations if (identityData.provider === 'email') { recommendations.push('Consider adding additional OAuth providers for comprehensive testing'); } if (!identityData.lastSignInAt) { recommendations.push('Consider setting lastSignInAt for realistic user activity patterns'); } return { isValid: errors.length === 0, errors, warnings, recommendations }; } /** * Find existing identity for user and provider */ async findExistingIdentity(userId, provider) { try { const { data, error } = await this.client .from('auth.identities') .select('*') .eq('user_id', userId) .eq('provider', provider) .single(); if (error) { if (error.code === 'PGRST116') { // No rows returned return null; } throw error; } return data; } catch (error) { logger_1.Logger.debug(`No existing identity found for user ${userId} with provider ${provider}`); return null; } } /** * Format identity data for auth.identities table structure */ formatIdentityRecord(identityData) { const now = new Date().toISOString(); return { id: `${identityData.userId}-${identityData.provider}`, user_id: identityData.userId, identity_data: { sub: identityData.providerId, email: identityData.email, ...identityData.providerMetadata }, provider: identityData.provider, last_sign_in_at: identityData.lastSignInAt || now, created_at: identityData.createdAt || now, updated_at: identityData.updatedAt || now }; } /** * Extract a reasonable name from email address */ extractNameFromEmail(email) { const localPart = email.split('@')[0]; // Handle common patterns if (localPart.includes('.')) { return localPart.split('.').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' '); } if (localPart.includes('_')) { return localPart.split('_').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' '); } // Just capitalize first letter return localPart.charAt(0).toUpperCase() + localPart.slice(1); } /** * Check if auth.identities table exists and is accessible */ async validateIdentityTableAccess() { const errors = []; const warnings = []; try { // Test table access const { error } = await this.client .from('auth.identities') .select('count') .limit(1); if (error) { if (error.message.includes('permission denied') || error.message.includes('unauthorized')) { errors.push('Insufficient permissions to access auth.identities table'); warnings.push('Ensure service role key is configured with proper auth schema permissions'); } else if (error.message.includes('does not exist')) { errors.push('auth.identities table does not exist'); warnings.push('This may indicate a non-standard MakerKit setup or older version'); } else { errors.push(`Table access error: ${error.message}`); } return { tableExists: false, hasPermissions: false, errors, warnings }; } logger_1.Logger.success('✅ auth.identities table access validated'); return { tableExists: true, hasPermissions: true, errors, warnings }; } catch (error) { errors.push(`Identity table validation failed: ${error.message}`); return { tableExists: false, hasPermissions: false, errors, warnings }; } } /** * Get identity statistics for debugging and validation */ async getIdentityStats() { try { // Get all identities const { data: identities, error } = await this.client .from('auth.identities') .select('user_id, provider'); if (error) { throw error; } const totalIdentities = identities?.length || 0; const providerBreakdown = {}; const userProviderCounts = {}; if (identities) { for (const identity of identities) { // Provider breakdown const provider = String(identity.provider); providerBreakdown[provider] = (providerBreakdown[provider] || 0) + 1; // User provider counts const userId = String(identity.user_id); userProviderCounts[userId] = (userProviderCounts[userId] || 0) + 1; } } const usersWithMultipleProviders = Object.values(userProviderCounts) .filter(count => count > 1).length; // Check for orphaned identities (identities without corresponding auth.users) let orphanedIdentities = 0; if (identities && identities.length > 0) { const userIds = [...new Set(identities.map(i => i.user_id))]; const { data: users } = await this.client .from('auth.users') .select('id') .in('id', userIds); const existingUserIds = new Set(users?.map(u => u.id) || []); orphanedIdentities = identities.filter(i => !existingUserIds.has(i.user_id)).length; } return { totalIdentities, providerBreakdown, usersWithMultipleProviders, orphanedIdentities }; } catch (error) { logger_1.Logger.error('Failed to get identity stats:', error); throw error; } } } exports.IdentityManager = IdentityManager; //# sourceMappingURL=identity-manager.js.map