feat: Add admin vehicle management and profile vehicles display (refs #11)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m34s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

- Add GET /api/admin/stats endpoint for Total Vehicles widget
- Add GET /api/admin/users/:auth0Sub/vehicles endpoint for user vehicle list
- Update AdminUsersPage with Total Vehicles stat and expandable vehicle rows
- Add My Vehicles section to SettingsPage (desktop) and MobileSettingsScreen
- Update AdminUsersMobileScreen with stats header and vehicle expansion
- Add defense-in-depth admin checks and error handling
- Update admin README documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-04 13:18:38 -06:00
parent 2ec208e25a
commit 4fc5b391e1
10 changed files with 639 additions and 67 deletions

View File

@@ -28,16 +28,112 @@ import { AdminService } from '../domain/admin.service';
export class UsersController {
private userProfileService: UserProfileService;
private adminService: AdminService;
private userProfileRepository: UserProfileRepository;
constructor() {
const userProfileRepository = new UserProfileRepository(pool);
this.userProfileRepository = new UserProfileRepository(pool);
const adminRepository = new AdminRepository(pool);
this.userProfileService = new UserProfileService(userProfileRepository);
this.userProfileService = new UserProfileService(this.userProfileRepository);
this.userProfileService.setAdminRepository(adminRepository);
this.adminService = new AdminService(adminRepository);
}
/**
* GET /api/admin/stats - Get admin dashboard stats
*/
async getAdminStats(
request: FastifyRequest,
reply: FastifyReply
) {
try {
const actorId = request.userContext?.userId;
if (!actorId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing',
});
}
// Defense-in-depth: verify admin status even with requireAdmin guard
if (!request.userContext?.isAdmin) {
return reply.code(403).send({
error: 'Forbidden',
message: 'Admin access required',
});
}
const [totalVehicles, totalUsers] = await Promise.all([
this.userProfileRepository.getTotalVehicleCount(),
this.userProfileRepository.getTotalUserCount(),
]);
return reply.code(200).send({
totalVehicles,
totalUsers,
});
} catch (error) {
logger.error('Error getting admin stats', {
error: error instanceof Error ? error.message : 'Unknown error',
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get admin stats',
});
}
}
/**
* GET /api/admin/users/:auth0Sub/vehicles - Get user's vehicles (admin view)
*/
async getUserVehicles(
request: FastifyRequest<{ Params: UserAuth0SubInput }>,
reply: FastifyReply
) {
try {
const actorId = request.userContext?.userId;
if (!actorId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing',
});
}
// Defense-in-depth: verify admin status even with requireAdmin guard
if (!request.userContext?.isAdmin) {
return reply.code(403).send({
error: 'Forbidden',
message: 'Admin access required',
});
}
// Validate path param
const parseResult = userAuth0SubSchema.safeParse(request.params);
if (!parseResult.success) {
return reply.code(400).send({
error: 'Validation error',
message: parseResult.error.errors.map(e => e.message).join(', '),
});
}
const { auth0Sub } = parseResult.data;
const vehicles = await this.userProfileRepository.getUserVehiclesForAdmin(auth0Sub);
return reply.code(200).send({ vehicles });
} catch (error) {
logger.error('Error getting user vehicles', {
error: error instanceof Error ? error.message : 'Unknown error',
auth0Sub: request.params?.auth0Sub,
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get user vehicles',
});
}
}
/**
* GET /api/admin/users - List all users with pagination and filters
*/