diff --git a/backend/src/core/plugins/auth.plugin.ts b/backend/src/core/plugins/auth.plugin.ts index 4b3a93d..11d3d49 100644 --- a/backend/src/core/plugins/auth.plugin.ts +++ b/backend/src/core/plugins/auth.plugin.ts @@ -166,9 +166,9 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { // If profile has placeholder email but we now have real email, update it if (profile.email.includes('@unknown.local') && email && !email.includes('@unknown.local')) { - await profileRepo.updateEmail(auth0Sub, email); + await profileRepo.updateEmail(userId, email); logger.info('Updated profile with correct email from Auth0', { - userId: auth0Sub.substring(0, 8) + '...', + userId: userId.substring(0, 8) + '...', }); } @@ -184,16 +184,16 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { try { const isVerifiedInAuth0 = await auth0ManagementClient.checkEmailVerified(auth0Sub); if (isVerifiedInAuth0 && !profile.emailVerified) { - await profileRepo.updateEmailVerified(auth0Sub, true); + await profileRepo.updateEmailVerified(userId, true); emailVerified = true; logger.info('Synced email verification status from Auth0', { - userId: auth0Sub.substring(0, 8) + '...', + userId: userId.substring(0, 8) + '...', }); } } catch (syncError) { // Don't fail auth if sync fails, just log logger.warn('Failed to sync email verification status', { - userId: auth0Sub.substring(0, 8) + '...', + userId: userId.substring(0, 8) + '...', error: syncError instanceof Error ? syncError.message : 'Unknown error', }); } diff --git a/backend/src/features/user-profile/data/user-profile.repository.ts b/backend/src/features/user-profile/data/user-profile.repository.ts index 2bcdf22..fcc78dd 100644 --- a/backend/src/features/user-profile/data/user-profile.repository.ts +++ b/backend/src/features/user-profile/data/user-profile.repository.ts @@ -44,6 +44,26 @@ export class UserProfileRepository { } } + async getById(id: string): Promise { + const query = ` + SELECT ${USER_PROFILE_COLUMNS} + FROM user_profiles + WHERE id = $1 + LIMIT 1 + `; + + try { + const result = await this.pool.query(query, [id]); + if (result.rows.length === 0) { + return null; + } + return this.mapRowToUserProfile(result.rows[0]); + } catch (error) { + logger.error('Error fetching user profile by id', { error, id }); + throw error; + } + } + async getByEmail(email: string): Promise { const query = ` SELECT ${USER_PROFILE_COLUMNS} @@ -94,7 +114,7 @@ export class UserProfileRepository { } async update( - auth0Sub: string, + userId: string, updates: { displayName?: string; notificationEmail?: string } ): Promise { const setClauses: string[] = []; @@ -115,12 +135,12 @@ export class UserProfileRepository { throw new Error('No fields to update'); } - values.push(auth0Sub); + values.push(userId); const query = ` UPDATE user_profiles SET ${setClauses.join(', ')} - WHERE auth0_sub = $${paramIndex} + WHERE id = $${paramIndex} RETURNING ${USER_PROFILE_COLUMNS} `; @@ -133,7 +153,7 @@ export class UserProfileRepository { return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error updating user profile', { error, auth0Sub, updates }); + logger.error('Error updating user profile', { error, userId, updates }); throw error; } } @@ -245,11 +265,11 @@ export class UserProfileRepository { au.auth0_sub as admin_auth0_sub, au.role as admin_role, (SELECT COUNT(*) FROM vehicles v - WHERE v.user_id = up.auth0_sub + WHERE v.user_id = up.id AND v.is_active = true AND v.deleted_at IS NULL) as vehicle_count FROM user_profiles up - LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL + LEFT JOIN admin_users au ON au.user_profile_id = up.id AND au.revoked_at IS NULL ${whereClause} ORDER BY ${sortColumn} ${sortOrder === 'desc' ? 'DESC' : 'ASC'} NULLS LAST LIMIT $${paramIndex} OFFSET $${paramIndex + 1} @@ -274,7 +294,7 @@ export class UserProfileRepository { /** * Get single user with admin status */ - async getUserWithAdminStatus(auth0Sub: string): Promise { + async getUserWithAdminStatus(userId: string): Promise { const query = ` SELECT up.id, up.auth0_sub, up.email, up.display_name, up.notification_email, @@ -283,23 +303,23 @@ export class UserProfileRepository { au.auth0_sub as admin_auth0_sub, au.role as admin_role, (SELECT COUNT(*) FROM vehicles v - WHERE v.user_id = up.auth0_sub + WHERE v.user_id = up.id AND v.is_active = true AND v.deleted_at IS NULL) as vehicle_count FROM user_profiles up - LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL - WHERE up.auth0_sub = $1 + LEFT JOIN admin_users au ON au.user_profile_id = up.id AND au.revoked_at IS NULL + WHERE up.id = $1 LIMIT 1 `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); if (result.rows.length === 0) { return null; } return this.mapRowToUserWithAdminStatus(result.rows[0]); } catch (error) { - logger.error('Error fetching user with admin status', { error, auth0Sub }); + logger.error('Error fetching user with admin status', { error, userId }); throw error; } } @@ -308,24 +328,24 @@ export class UserProfileRepository { * Update user subscription tier */ async updateSubscriptionTier( - auth0Sub: string, + userId: string, tier: SubscriptionTier ): Promise { const query = ` UPDATE user_profiles SET subscription_tier = $1 - WHERE auth0_sub = $2 + WHERE id = $2 RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [tier, auth0Sub]); + const result = await this.pool.query(query, [tier, userId]); if (result.rows.length === 0) { throw new Error('User profile not found'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error updating subscription tier', { error, auth0Sub, tier }); + logger.error('Error updating subscription tier', { error, userId, tier }); throw error; } } @@ -333,22 +353,22 @@ export class UserProfileRepository { /** * Deactivate user (soft delete) */ - async deactivateUser(auth0Sub: string, deactivatedBy: string): Promise { + async deactivateUser(userId: string, deactivatedBy: string): Promise { const query = ` UPDATE user_profiles SET deactivated_at = NOW(), deactivated_by = $1 - WHERE auth0_sub = $2 AND deactivated_at IS NULL + WHERE id = $2 AND deactivated_at IS NULL RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [deactivatedBy, auth0Sub]); + const result = await this.pool.query(query, [deactivatedBy, userId]); if (result.rows.length === 0) { throw new Error('User profile not found or already deactivated'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error deactivating user', { error, auth0Sub, deactivatedBy }); + logger.error('Error deactivating user', { error, userId, deactivatedBy }); throw error; } } @@ -356,22 +376,22 @@ export class UserProfileRepository { /** * Reactivate user */ - async reactivateUser(auth0Sub: string): Promise { + async reactivateUser(userId: string): Promise { const query = ` UPDATE user_profiles SET deactivated_at = NULL, deactivated_by = NULL - WHERE auth0_sub = $1 AND deactivated_at IS NOT NULL + WHERE id = $1 AND deactivated_at IS NOT NULL RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); if (result.rows.length === 0) { throw new Error('User profile not found or not deactivated'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error reactivating user', { error, auth0Sub }); + logger.error('Error reactivating user', { error, userId }); throw error; } } @@ -380,7 +400,7 @@ export class UserProfileRepository { * Admin update of user profile (can update email and displayName) */ async adminUpdateProfile( - auth0Sub: string, + userId: string, updates: { email?: string; displayName?: string } ): Promise { const setClauses: string[] = []; @@ -401,12 +421,12 @@ export class UserProfileRepository { throw new Error('No fields to update'); } - values.push(auth0Sub); + values.push(userId); const query = ` UPDATE user_profiles SET ${setClauses.join(', ')}, updated_at = NOW() - WHERE auth0_sub = $${paramIndex} + WHERE id = $${paramIndex} RETURNING ${USER_PROFILE_COLUMNS} `; @@ -419,7 +439,7 @@ export class UserProfileRepository { return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error admin updating user profile', { error, auth0Sub, updates }); + logger.error('Error admin updating user profile', { error, userId, updates }); throw error; } } @@ -427,22 +447,22 @@ export class UserProfileRepository { /** * Update email verification status */ - async updateEmailVerified(auth0Sub: string, emailVerified: boolean): Promise { + async updateEmailVerified(userId: string, emailVerified: boolean): Promise { const query = ` UPDATE user_profiles SET email_verified = $1, updated_at = NOW() - WHERE auth0_sub = $2 + WHERE id = $2 RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [emailVerified, auth0Sub]); + const result = await this.pool.query(query, [emailVerified, userId]); if (result.rows.length === 0) { throw new Error('User profile not found'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error updating email verified status', { error, auth0Sub, emailVerified }); + logger.error('Error updating email verified status', { error, userId, emailVerified }); throw error; } } @@ -450,19 +470,19 @@ export class UserProfileRepository { /** * Mark onboarding as complete */ - async markOnboardingComplete(auth0Sub: string): Promise { + async markOnboardingComplete(userId: string): Promise { const query = ` UPDATE user_profiles SET onboarding_completed_at = NOW(), updated_at = NOW() - WHERE auth0_sub = $1 AND onboarding_completed_at IS NULL + WHERE id = $1 AND onboarding_completed_at IS NULL RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); if (result.rows.length === 0) { // Check if already completed or profile not found - const existing = await this.getByAuth0Sub(auth0Sub); + const existing = await this.getById(userId); if (existing && existing.onboardingCompletedAt) { return existing; // Already completed, return as-is } @@ -470,7 +490,7 @@ export class UserProfileRepository { } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error marking onboarding complete', { error, auth0Sub }); + logger.error('Error marking onboarding complete', { error, userId }); throw error; } } @@ -478,22 +498,22 @@ export class UserProfileRepository { /** * Update user email (used when fetching correct email from Auth0) */ - async updateEmail(auth0Sub: string, email: string): Promise { + async updateEmail(userId: string, email: string): Promise { const query = ` UPDATE user_profiles SET email = $1, updated_at = NOW() - WHERE auth0_sub = $2 + WHERE id = $2 RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [email, auth0Sub]); + const result = await this.pool.query(query, [email, userId]); if (result.rows.length === 0) { throw new Error('User profile not found'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error updating user email', { error, auth0Sub }); + logger.error('Error updating user email', { error, userId }); throw error; } } @@ -502,7 +522,7 @@ export class UserProfileRepository { * Request account deletion (sets deletion timestamps and deactivates account) * 30-day grace period before permanent deletion */ - async requestDeletion(auth0Sub: string): Promise { + async requestDeletion(userId: string): Promise { const query = ` UPDATE user_profiles SET @@ -510,18 +530,18 @@ export class UserProfileRepository { deletion_scheduled_for = NOW() + INTERVAL '30 days', deactivated_at = NOW(), updated_at = NOW() - WHERE auth0_sub = $1 AND deletion_requested_at IS NULL + WHERE id = $1 AND deletion_requested_at IS NULL RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); if (result.rows.length === 0) { throw new Error('User profile not found or deletion already requested'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error requesting account deletion', { error, auth0Sub }); + logger.error('Error requesting account deletion', { error, userId }); throw error; } } @@ -529,7 +549,7 @@ export class UserProfileRepository { /** * Cancel deletion request (clears deletion timestamps and reactivates account) */ - async cancelDeletion(auth0Sub: string): Promise { + async cancelDeletion(userId: string): Promise { const query = ` UPDATE user_profiles SET @@ -538,18 +558,18 @@ export class UserProfileRepository { deactivated_at = NULL, deactivated_by = NULL, updated_at = NOW() - WHERE auth0_sub = $1 AND deletion_requested_at IS NOT NULL + WHERE id = $1 AND deletion_requested_at IS NOT NULL RETURNING ${USER_PROFILE_COLUMNS} `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); if (result.rows.length === 0) { throw new Error('User profile not found or no deletion request pending'); } return this.mapRowToUserProfile(result.rows[0]); } catch (error) { - logger.error('Error canceling account deletion', { error, auth0Sub }); + logger.error('Error canceling account deletion', { error, userId }); throw error; } } @@ -579,7 +599,7 @@ export class UserProfileRepository { * Hard delete user and all associated data * This is a permanent operation - use with caution */ - async hardDeleteUser(auth0Sub: string): Promise { + async hardDeleteUser(userId: string): Promise { const client = await this.pool.connect(); try { @@ -590,51 +610,51 @@ export class UserProfileRepository { `UPDATE community_stations SET submitted_by = 'deleted-user' WHERE submitted_by = $1`, - [auth0Sub] + [userId] ); // 2. Delete notification logs await client.query( 'DELETE FROM notification_logs WHERE user_id = $1', - [auth0Sub] + [userId] ); // 3. Delete user notifications await client.query( 'DELETE FROM user_notifications WHERE user_id = $1', - [auth0Sub] + [userId] ); // 4. Delete saved stations await client.query( 'DELETE FROM saved_stations WHERE user_id = $1', - [auth0Sub] + [userId] ); // 5. Delete vehicles (cascades to fuel_logs, maintenance_logs, maintenance_schedules, documents) await client.query( 'DELETE FROM vehicles WHERE user_id = $1', - [auth0Sub] + [userId] ); // 6. Delete user preferences await client.query( 'DELETE FROM user_preferences WHERE user_id = $1', - [auth0Sub] + [userId] ); // 7. Delete user profile (final step) await client.query( - 'DELETE FROM user_profiles WHERE auth0_sub = $1', - [auth0Sub] + 'DELETE FROM user_profiles WHERE id = $1', + [userId] ); await client.query('COMMIT'); - logger.info('User hard deleted successfully', { auth0Sub }); + logger.info('User hard deleted successfully', { userId }); } catch (error) { await client.query('ROLLBACK'); - logger.error('Error hard deleting user', { error, auth0Sub }); + logger.error('Error hard deleting user', { error, userId }); throw error; } finally { client.release(); @@ -686,7 +706,7 @@ export class UserProfileRepository { * Get vehicles for a user (admin view) * Returns only year, make, model for privacy */ - async getUserVehiclesForAdmin(auth0Sub: string): Promise> { + async getUserVehiclesForAdmin(userId: string): Promise> { const query = ` SELECT year, make, model FROM vehicles @@ -697,14 +717,14 @@ export class UserProfileRepository { `; try { - const result = await this.pool.query(query, [auth0Sub]); + const result = await this.pool.query(query, [userId]); return result.rows.map(row => ({ year: row.year, make: row.make, model: row.model, })); } catch (error) { - logger.error('Error getting user vehicles for admin', { error, auth0Sub }); + logger.error('Error getting user vehicles for admin', { error, userId }); throw error; } }