feat: delete users - not tested
This commit is contained in:
@@ -16,7 +16,8 @@ import { logger } from '../../../core/logging/logger';
|
||||
// Base columns for user profile queries
|
||||
const USER_PROFILE_COLUMNS = `
|
||||
id, auth0_sub, email, display_name, notification_email,
|
||||
subscription_tier, deactivated_at, deactivated_by,
|
||||
subscription_tier, email_verified, onboarding_completed_at,
|
||||
deactivated_at, deactivated_by, deletion_requested_at, deletion_scheduled_for,
|
||||
created_at, updated_at
|
||||
`;
|
||||
|
||||
@@ -139,8 +140,12 @@ export class UserProfileRepository {
|
||||
displayName: row.display_name,
|
||||
notificationEmail: row.notification_email,
|
||||
subscriptionTier: row.subscription_tier || 'free',
|
||||
emailVerified: row.email_verified ?? false,
|
||||
onboardingCompletedAt: row.onboarding_completed_at ? new Date(row.onboarding_completed_at) : null,
|
||||
deactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,
|
||||
deactivatedBy: row.deactivated_by || null,
|
||||
deletionRequestedAt: row.deletion_requested_at ? new Date(row.deletion_requested_at) : null,
|
||||
deletionScheduledFor: row.deletion_scheduled_for ? new Date(row.deletion_scheduled_for) : null,
|
||||
createdAt: new Date(row.created_at),
|
||||
updatedAt: new Date(row.updated_at),
|
||||
};
|
||||
@@ -213,8 +218,8 @@ export class UserProfileRepository {
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
up.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
|
||||
up.subscription_tier, up.deactivated_at, up.deactivated_by,
|
||||
up.created_at, up.updated_at,
|
||||
up.subscription_tier, up.email_verified, up.onboarding_completed_at,
|
||||
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at,
|
||||
au.auth0_sub as admin_auth0_sub,
|
||||
au.role as admin_role
|
||||
FROM user_profiles up
|
||||
@@ -247,8 +252,8 @@ export class UserProfileRepository {
|
||||
const query = `
|
||||
SELECT
|
||||
up.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
|
||||
up.subscription_tier, up.deactivated_at, up.deactivated_by,
|
||||
up.created_at, up.updated_at,
|
||||
up.subscription_tier, up.email_verified, up.onboarding_completed_at,
|
||||
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at,
|
||||
au.auth0_sub as admin_auth0_sub,
|
||||
au.role as admin_role
|
||||
FROM user_profiles up
|
||||
@@ -388,4 +393,221 @@ export class UserProfileRepository {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update email verification status
|
||||
*/
|
||||
async updateEmailVerified(auth0Sub: string, emailVerified: boolean): Promise<UserProfile> {
|
||||
const query = `
|
||||
UPDATE user_profiles
|
||||
SET email_verified = $1, updated_at = NOW()
|
||||
WHERE auth0_sub = $2
|
||||
RETURNING ${USER_PROFILE_COLUMNS}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [emailVerified, auth0Sub]);
|
||||
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 });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark onboarding as complete
|
||||
*/
|
||||
async markOnboardingComplete(auth0Sub: string): Promise<UserProfile> {
|
||||
const query = `
|
||||
UPDATE user_profiles
|
||||
SET onboarding_completed_at = NOW(), updated_at = NOW()
|
||||
WHERE auth0_sub = $1 AND onboarding_completed_at IS NULL
|
||||
RETURNING ${USER_PROFILE_COLUMNS}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [auth0Sub]);
|
||||
if (result.rows.length === 0) {
|
||||
// Check if already completed or profile not found
|
||||
const existing = await this.getByAuth0Sub(auth0Sub);
|
||||
if (existing && existing.onboardingCompletedAt) {
|
||||
return existing; // Already completed, return as-is
|
||||
}
|
||||
throw new Error('User profile not found');
|
||||
}
|
||||
return this.mapRowToUserProfile(result.rows[0]);
|
||||
} catch (error) {
|
||||
logger.error('Error marking onboarding complete', { error, auth0Sub });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user email (used when fetching correct email from Auth0)
|
||||
*/
|
||||
async updateEmail(auth0Sub: string, email: string): Promise<UserProfile> {
|
||||
const query = `
|
||||
UPDATE user_profiles
|
||||
SET email = $1, updated_at = NOW()
|
||||
WHERE auth0_sub = $2
|
||||
RETURNING ${USER_PROFILE_COLUMNS}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [email, auth0Sub]);
|
||||
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 });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request account deletion (sets deletion timestamps and deactivates account)
|
||||
* 30-day grace period before permanent deletion
|
||||
*/
|
||||
async requestDeletion(auth0Sub: string): Promise<UserProfile> {
|
||||
const query = `
|
||||
UPDATE user_profiles
|
||||
SET
|
||||
deletion_requested_at = NOW(),
|
||||
deletion_scheduled_for = NOW() + INTERVAL '30 days',
|
||||
deactivated_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE auth0_sub = $1 AND deletion_requested_at IS NULL
|
||||
RETURNING ${USER_PROFILE_COLUMNS}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [auth0Sub]);
|
||||
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 });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel deletion request (clears deletion timestamps and reactivates account)
|
||||
*/
|
||||
async cancelDeletion(auth0Sub: string): Promise<UserProfile> {
|
||||
const query = `
|
||||
UPDATE user_profiles
|
||||
SET
|
||||
deletion_requested_at = NULL,
|
||||
deletion_scheduled_for = NULL,
|
||||
deactivated_at = NULL,
|
||||
deactivated_by = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE auth0_sub = $1 AND deletion_requested_at IS NOT NULL
|
||||
RETURNING ${USER_PROFILE_COLUMNS}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query, [auth0Sub]);
|
||||
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 });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users whose deletion grace period has expired
|
||||
*/
|
||||
async getUsersPastGracePeriod(): Promise<UserProfile[]> {
|
||||
const query = `
|
||||
SELECT ${USER_PROFILE_COLUMNS}
|
||||
FROM user_profiles
|
||||
WHERE deletion_scheduled_for IS NOT NULL
|
||||
AND deletion_scheduled_for <= NOW()
|
||||
ORDER BY deletion_scheduled_for ASC
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query);
|
||||
return result.rows.map(row => this.mapRowToUserProfile(row));
|
||||
} catch (error) {
|
||||
logger.error('Error fetching users past grace period', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard delete user and all associated data
|
||||
* This is a permanent operation - use with caution
|
||||
*/
|
||||
async hardDeleteUser(auth0Sub: string): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Anonymize community station submissions (keep data but remove user reference)
|
||||
await client.query(
|
||||
`UPDATE community_stations
|
||||
SET submitted_by = 'deleted-user'
|
||||
WHERE submitted_by = $1`,
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 2. Delete notification logs
|
||||
await client.query(
|
||||
'DELETE FROM notification_logs WHERE user_id = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 3. Delete user notifications
|
||||
await client.query(
|
||||
'DELETE FROM user_notifications WHERE user_id = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 4. Delete saved stations
|
||||
await client.query(
|
||||
'DELETE FROM saved_stations WHERE user_id = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 5. Delete vehicles (cascades to fuel_logs, maintenance_logs, maintenance_schedules, documents)
|
||||
await client.query(
|
||||
'DELETE FROM vehicles WHERE user_id = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 6. Delete user preferences
|
||||
await client.query(
|
||||
'DELETE FROM user_preferences WHERE user_id = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
// 7. Delete user profile (final step)
|
||||
await client.query(
|
||||
'DELETE FROM user_profiles WHERE auth0_sub = $1',
|
||||
[auth0Sub]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
logger.info('User hard deleted successfully', { auth0Sub });
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Error hard deleting user', { error, auth0Sub });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user