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