Migrate admin controller, routes, validation, and users controller from auth0Sub identifiers to UUID. Admin CRUD now uses admin UUID id, user management routes use user_profiles UUID. Clean up debug logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
253 lines
7.5 KiB
TypeScript
253 lines
7.5 KiB
TypeScript
/**
|
|
* @ai-summary Admin user data access layer
|
|
* @ai-context Provides parameterized SQL queries for admin user operations
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import { AdminUser, AdminAuditLog } from '../domain/admin.types';
|
|
import { logger } from '../../../core/logging/logger';
|
|
|
|
export class AdminRepository {
|
|
constructor(private pool: Pool) {}
|
|
|
|
async getAdminById(id: string): Promise<AdminUser | null> {
|
|
const query = `
|
|
SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
FROM admin_users
|
|
WHERE id = $1
|
|
LIMIT 1
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [id]);
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error fetching admin by id', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAdminByUserProfileId(userProfileId: string): Promise<AdminUser | null> {
|
|
const query = `
|
|
SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
FROM admin_users
|
|
WHERE user_profile_id = $1
|
|
LIMIT 1
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [userProfileId]);
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error fetching admin by user_profile_id', { error, userProfileId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAdminByEmail(email: string): Promise<AdminUser | null> {
|
|
const query = `
|
|
SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
FROM admin_users
|
|
WHERE LOWER(email) = LOWER($1)
|
|
LIMIT 1
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [email]);
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error fetching admin by email', { error, email });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAllAdmins(): Promise<AdminUser[]> {
|
|
const query = `
|
|
SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
FROM admin_users
|
|
ORDER BY created_at DESC
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query);
|
|
return result.rows.map(row => this.mapRowToAdminUser(row));
|
|
} catch (error) {
|
|
logger.error('Error fetching all admins', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getActiveAdmins(): Promise<AdminUser[]> {
|
|
const query = `
|
|
SELECT id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
FROM admin_users
|
|
WHERE revoked_at IS NULL
|
|
ORDER BY created_at DESC
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query);
|
|
return result.rows.map(row => this.mapRowToAdminUser(row));
|
|
} catch (error) {
|
|
logger.error('Error fetching active admins', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createAdmin(userProfileId: string, email: string, role: string, createdBy: string): Promise<AdminUser> {
|
|
const query = `
|
|
INSERT INTO admin_users (user_profile_id, email, role, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [userProfileId, email, role, createdBy]);
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Failed to create admin user');
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error creating admin', { error, userProfileId, email });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async revokeAdmin(id: string): Promise<AdminUser> {
|
|
const query = `
|
|
UPDATE admin_users
|
|
SET revoked_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [id]);
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Admin user not found');
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error revoking admin', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async reinstateAdmin(id: string): Promise<AdminUser> {
|
|
const query = `
|
|
UPDATE admin_users
|
|
SET revoked_at = NULL
|
|
WHERE id = $1
|
|
RETURNING id, user_profile_id, email, role, created_at, created_by, revoked_at, updated_at
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [id]);
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Admin user not found');
|
|
}
|
|
return this.mapRowToAdminUser(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error reinstating admin', { error, id });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async logAuditAction(
|
|
actorAdminId: string,
|
|
action: string,
|
|
targetAdminId?: string,
|
|
resourceType?: string,
|
|
resourceId?: string,
|
|
context?: Record<string, any>
|
|
): Promise<AdminAuditLog> {
|
|
const query = `
|
|
INSERT INTO admin_audit_logs (actor_admin_id, target_admin_id, action, resource_type, resource_id, context)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, actor_admin_id, target_admin_id, action, resource_type, resource_id, context, created_at
|
|
`;
|
|
|
|
try {
|
|
const result = await this.pool.query(query, [
|
|
actorAdminId,
|
|
targetAdminId || null,
|
|
action,
|
|
resourceType || null,
|
|
resourceId || null,
|
|
context ? JSON.stringify(context) : null,
|
|
]);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new Error('Failed to create audit log');
|
|
}
|
|
|
|
return this.mapRowToAuditLog(result.rows[0]);
|
|
} catch (error) {
|
|
logger.error('Error logging audit action', { error, actorAdminId, action });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAuditLogs(limit: number = 100, offset: number = 0): Promise<{ logs: AdminAuditLog[]; total: number }> {
|
|
const countQuery = 'SELECT COUNT(*) as total FROM admin_audit_logs';
|
|
const query = `
|
|
SELECT id, actor_admin_id, target_admin_id, action, resource_type, resource_id, context, created_at
|
|
FROM admin_audit_logs
|
|
ORDER BY created_at DESC
|
|
LIMIT $1 OFFSET $2
|
|
`;
|
|
|
|
try {
|
|
const [countResult, dataResult] = await Promise.all([
|
|
this.pool.query(countQuery),
|
|
this.pool.query(query, [limit, offset]),
|
|
]);
|
|
|
|
const total = parseInt(countResult.rows[0].total, 10);
|
|
const logs = dataResult.rows.map(row => this.mapRowToAuditLog(row));
|
|
|
|
return { logs, total };
|
|
} catch (error) {
|
|
logger.error('Error fetching audit logs', { error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
private mapRowToAdminUser(row: any): AdminUser {
|
|
return {
|
|
id: row.id,
|
|
userProfileId: row.user_profile_id,
|
|
email: row.email,
|
|
role: row.role,
|
|
createdAt: new Date(row.created_at),
|
|
createdBy: row.created_by,
|
|
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null,
|
|
updatedAt: new Date(row.updated_at),
|
|
};
|
|
}
|
|
|
|
private mapRowToAuditLog(row: any): AdminAuditLog {
|
|
return {
|
|
id: row.id,
|
|
actorAdminId: row.actor_admin_id,
|
|
targetAdminId: row.target_admin_id,
|
|
action: row.action,
|
|
resourceType: row.resource_type,
|
|
resourceId: row.resource_id,
|
|
// JSONB columns are automatically parsed by pg driver - no JSON.parse needed
|
|
context: row.context || undefined,
|
|
createdAt: new Date(row.created_at),
|
|
};
|
|
}
|
|
}
|