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>
732 lines
21 KiB
TypeScript
732 lines
21 KiB
TypeScript
/**
|
|
* @ai-summary User profile data access layer
|
|
* @ai-context Provides parameterized SQL queries for user profile operations
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import {
|
|
UserProfile,
|
|
UserWithAdminStatus,
|
|
ListUsersQuery,
|
|
ListUsersResponse,
|
|
SubscriptionTier,
|
|
} from '../domain/user-profile.types';
|
|
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, email_verified, onboarding_completed_at,
|
|
deactivated_at, deactivated_by, deletion_requested_at, deletion_scheduled_for,
|
|
created_at, updated_at
|
|
`;
|
|
|
|
export class UserProfileRepository {
|
|
constructor(private pool: Pool) {}
|
|
|
|
async getByAuth0Sub(auth0Sub: string): Promise<UserProfile | null> {
|
|
const query = `
|
|
SELECT ${USER_PROFILE_COLUMNS}
|
|
FROM user_profiles
|
|
WHERE auth0_sub = $1
|
|
LIMIT 1
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [auth0Sub]);
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
return this.mapRowToUserProfile(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error fetching user profile by auth0_sub', { error, auth0Sub });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
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> {
|
|
const query = `
|
|
SELECT ${USER_PROFILE_COLUMNS}
|
|
FROM user_profiles
|
|
WHERE email = $1
|
|
LIMIT 1
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [email]);
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
return this.mapRowToUserProfile(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error fetching user profile by email', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async create(
|
|
auth0Sub: string,
|
|
email: string,
|
|
displayName?: string
|
|
): Promise<UserProfile> {
|
|
const query = `
|
|
INSERT INTO user_profiles (auth0_sub, email, display_name, subscription_tier)
|
|
VALUES ($1, $2, $3, 'free')
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [
|
|
auth0Sub,
|
|
email,
|
|
displayName || null,
|
|
]);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Failed to create user profile');
|
|
}
|
|
|
|
return this.mapRowToUserProfile(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error creating user profile', { error, auth0Sub, email });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async update(
|
|
userId: string,
|
|
updates: { displayName?: string; notificationEmail?: string }
|
|
): Promise<UserProfile> {
|
|
const setClauses: string[] = [];
|
|
const values: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (updates.displayName !== undefined) {
|
|
setClauses.push(`display_name = $${paramIndex++}`);
|
|
values.push(updates.displayName);
|
|
}
|
|
|
|
if (updates.notificationEmail !== undefined) {
|
|
setClauses.push(`notification_email = $${paramIndex++}`);
|
|
values.push(updates.notificationEmail);
|
|
}
|
|
|
|
if (setClauses.length === 0) {
|
|
throw new Error('No fields to update');
|
|
}
|
|
|
|
values.push(userId);
|
|
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET ${setClauses.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, values);
|
|
|
|
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 profile', { error, userId, updates });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getOrCreate(
|
|
auth0Sub: string,
|
|
defaults: { email: string; displayName?: string }
|
|
): Promise<UserProfile> {
|
|
// Try to find existing profile
|
|
const existing = await this.getByAuth0Sub(auth0Sub);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
// Create new profile if not found
|
|
return await this.create(auth0Sub, defaults.email, defaults.displayName);
|
|
}
|
|
|
|
private mapRowToUserProfile(row: any): UserProfile {
|
|
return {
|
|
id: row.id,
|
|
auth0Sub: row.auth0_sub,
|
|
email: row.email,
|
|
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),
|
|
};
|
|
}
|
|
|
|
private mapRowToUserWithAdminStatus(row: any): UserWithAdminStatus {
|
|
return {
|
|
...this.mapRowToUserProfile(row),
|
|
isAdmin: !!row.admin_auth0_sub,
|
|
adminRole: row.admin_role || null,
|
|
vehicleCount: parseInt(row.vehicle_count, 10) || 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* List all users with pagination, search, and filters
|
|
* Includes admin status by joining with admin_users table
|
|
*/
|
|
async listAllUsers(query: ListUsersQuery): Promise<ListUsersResponse> {
|
|
const page = query.page || 1;
|
|
const pageSize = query.pageSize || 20;
|
|
const offset = (page - 1) * pageSize;
|
|
const sortBy = query.sortBy || 'createdAt';
|
|
const sortOrder = query.sortOrder || 'desc';
|
|
|
|
// Map sortBy to column names
|
|
const sortColumnMap: Record<string, string> = {
|
|
email: 'up.email',
|
|
createdAt: 'up.created_at',
|
|
displayName: 'up.display_name',
|
|
subscriptionTier: 'up.subscription_tier',
|
|
vehicleCount: 'vehicle_count',
|
|
};
|
|
const sortColumn = sortColumnMap[sortBy] || 'up.created_at';
|
|
|
|
const whereClauses: string[] = [];
|
|
const values: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// Search filter (email or display_name)
|
|
if (query.search) {
|
|
whereClauses.push(`(up.email ILIKE $${paramIndex} OR up.display_name ILIKE $${paramIndex})`);
|
|
values.push(`%${query.search}%`);
|
|
paramIndex++;
|
|
}
|
|
|
|
// Tier filter
|
|
if (query.tier) {
|
|
whereClauses.push(`up.subscription_tier = $${paramIndex}`);
|
|
values.push(query.tier);
|
|
paramIndex++;
|
|
}
|
|
|
|
// Status filter
|
|
if (query.status === 'active') {
|
|
whereClauses.push('up.deactivated_at IS NULL');
|
|
} else if (query.status === 'deactivated') {
|
|
whereClauses.push('up.deactivated_at IS NOT NULL');
|
|
}
|
|
// 'all' means no filter
|
|
|
|
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
|
|
|
// Count query
|
|
const countQuery = `
|
|
SELECT COUNT(*) as total
|
|
FROM user_profiles up
|
|
${whereClause}
|
|
`;
|
|
|
|
// Data query with admin status join and vehicle count
|
|
const dataQuery = `
|
|
SELECT
|
|
up.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
|
|
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,
|
|
(SELECT COUNT(*) FROM vehicles v
|
|
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 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}
|
|
`;
|
|
|
|
try {
|
|
const [countResult, dataResult] = await Promise.all([
|
|
this.pool.query(countQuery, values),
|
|
this.pool.query(dataQuery, [...values, pageSize, offset]),
|
|
]);
|
|
|
|
const total = parseInt(countResult.rows[0]?.total || '0', 10);
|
|
const users = dataResult.rows.map((row) => this.mapRowToUserWithAdminStatus(row));
|
|
|
|
return { users, total, page, pageSize };
|
|
} catch (error) {
|
|
logger.error('Error listing users', { error, query });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get single user with admin status
|
|
*/
|
|
async getUserWithAdminStatus(userId: string): Promise<UserWithAdminStatus | null> {
|
|
const query = `
|
|
SELECT
|
|
up.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
|
|
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,
|
|
(SELECT COUNT(*) FROM vehicles v
|
|
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 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, [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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user subscription tier
|
|
*/
|
|
async updateSubscriptionTier(
|
|
userId: string,
|
|
tier: SubscriptionTier
|
|
): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET subscription_tier = $1
|
|
WHERE id = $2
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId, tier });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deactivate user (soft delete)
|
|
*/
|
|
async deactivateUser(userId: string, deactivatedBy: string): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET deactivated_at = NOW(), deactivated_by = $1
|
|
WHERE id = $2 AND deactivated_at IS NULL
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId, deactivatedBy });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reactivate user
|
|
*/
|
|
async reactivateUser(userId: string): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET deactivated_at = NULL, deactivated_by = NULL
|
|
WHERE id = $1 AND deactivated_at IS NOT NULL
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admin update of user profile (can update email and displayName)
|
|
*/
|
|
async adminUpdateProfile(
|
|
userId: string,
|
|
updates: { email?: string; displayName?: string }
|
|
): Promise<UserProfile> {
|
|
const setClauses: string[] = [];
|
|
const values: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (updates.email !== undefined) {
|
|
setClauses.push(`email = $${paramIndex++}`);
|
|
values.push(updates.email);
|
|
}
|
|
|
|
if (updates.displayName !== undefined) {
|
|
setClauses.push(`display_name = $${paramIndex++}`);
|
|
values.push(updates.displayName);
|
|
}
|
|
|
|
if (setClauses.length === 0) {
|
|
throw new Error('No fields to update');
|
|
}
|
|
|
|
values.push(userId);
|
|
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET ${setClauses.join(', ')}, updated_at = NOW()
|
|
WHERE id = $${paramIndex}
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('User profile not found');
|
|
}
|
|
|
|
return this.mapRowToUserProfile(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error admin updating user profile', { error, userId, updates });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update email verification status
|
|
*/
|
|
async updateEmailVerified(userId: string, emailVerified: boolean): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET email_verified = $1, updated_at = NOW()
|
|
WHERE id = $2
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId, emailVerified });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark onboarding as complete
|
|
*/
|
|
async markOnboardingComplete(userId: string): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET onboarding_completed_at = NOW(), updated_at = NOW()
|
|
WHERE id = $1 AND onboarding_completed_at IS NULL
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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.getById(userId);
|
|
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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user email (used when fetching correct email from Auth0)
|
|
*/
|
|
async updateEmail(userId: string, email: string): Promise<UserProfile> {
|
|
const query = `
|
|
UPDATE user_profiles
|
|
SET email = $1, updated_at = NOW()
|
|
WHERE id = $2
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request account deletion (sets deletion timestamps and deactivates account)
|
|
* 30-day grace period before permanent deletion
|
|
*/
|
|
async requestDeletion(userId: 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 id = $1 AND deletion_requested_at IS NULL
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel deletion request (clears deletion timestamps and reactivates account)
|
|
*/
|
|
async cancelDeletion(userId: 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 id = $1 AND deletion_requested_at IS NOT NULL
|
|
RETURNING ${USER_PROFILE_COLUMNS}
|
|
`;
|
|
|
|
try {
|
|
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, userId });
|
|
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(userId: 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`,
|
|
[userId]
|
|
);
|
|
|
|
// 2. Delete notification logs
|
|
await client.query(
|
|
'DELETE FROM notification_logs WHERE user_id = $1',
|
|
[userId]
|
|
);
|
|
|
|
// 3. Delete user notifications
|
|
await client.query(
|
|
'DELETE FROM user_notifications WHERE user_id = $1',
|
|
[userId]
|
|
);
|
|
|
|
// 4. Delete saved stations
|
|
await client.query(
|
|
'DELETE FROM saved_stations WHERE user_id = $1',
|
|
[userId]
|
|
);
|
|
|
|
// 5. Delete vehicles (cascades to fuel_logs, maintenance_logs, maintenance_schedules, documents)
|
|
await client.query(
|
|
'DELETE FROM vehicles WHERE user_id = $1',
|
|
[userId]
|
|
);
|
|
|
|
// 6. Delete user preferences
|
|
await client.query(
|
|
'DELETE FROM user_preferences WHERE user_id = $1',
|
|
[userId]
|
|
);
|
|
|
|
// 7. Delete user profile (final step)
|
|
await client.query(
|
|
'DELETE FROM user_profiles WHERE id = $1',
|
|
[userId]
|
|
);
|
|
|
|
await client.query('COMMIT');
|
|
|
|
logger.info('User hard deleted successfully', { userId });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
logger.error('Error hard deleting user', { error, userId });
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get total count of active vehicles across all users
|
|
* Used by admin stats endpoint for dashboard widget
|
|
*/
|
|
async getTotalVehicleCount(): Promise<number> {
|
|
const query = `
|
|
SELECT COUNT(*) as total
|
|
FROM vehicles
|
|
WHERE is_active = true
|
|
AND deleted_at IS NULL
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query);
|
|
return parseInt(result.rows[0]?.total || '0', 10);
|
|
} catch (error) {
|
|
logger.error('Error getting total vehicle count', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get total count of active users
|
|
* Used by admin stats endpoint for dashboard widget
|
|
*/
|
|
async getTotalUserCount(): Promise<number> {
|
|
const query = `
|
|
SELECT COUNT(*) as total
|
|
FROM user_profiles
|
|
WHERE deactivated_at IS NULL
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query);
|
|
return parseInt(result.rows[0]?.total || '0', 10);
|
|
} catch (error) {
|
|
logger.error('Error getting total user count', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get vehicles for a user (admin view)
|
|
* Returns only year, make, model for privacy
|
|
*/
|
|
async getUserVehiclesForAdmin(userId: string): Promise<Array<{ year: number; make: string; model: string }>> {
|
|
const query = `
|
|
SELECT year, make, model
|
|
FROM vehicles
|
|
WHERE user_id = $1
|
|
AND is_active = true
|
|
AND deleted_at IS NULL
|
|
ORDER BY year DESC, make ASC, model ASC
|
|
`;
|
|
|
|
try {
|
|
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, userId });
|
|
throw error;
|
|
}
|
|
}
|
|
}
|