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
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:
@@ -62,14 +62,56 @@ Provides:
|
||||
- `admin-guard` plugin - Authorization enforcement (decorator on Fastify)
|
||||
- `request.userContext` - Enhanced with `isAdmin`, `adminRecord`
|
||||
|
||||
### Phase 2: Admin Management APIs
|
||||
### Admin Dashboard Stats
|
||||
|
||||
Will provide:
|
||||
- `/api/admin/admins` - List all admins (GET)
|
||||
- `/api/admin/admins` - Add admin (POST)
|
||||
- `/api/admin/admins/:auth0Sub/revoke` - Revoke admin (PATCH)
|
||||
- `/api/admin/admins/:auth0Sub/reinstate` - Reinstate admin (PATCH)
|
||||
- `/api/admin/audit-logs` - View audit trail (GET)
|
||||
Provides admin dashboard statistics:
|
||||
|
||||
- `GET /api/admin/stats` - Get total users and vehicles counts
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"totalUsers": 150,
|
||||
"totalVehicles": 287
|
||||
}
|
||||
```
|
||||
|
||||
### User Management APIs
|
||||
|
||||
Provides:
|
||||
- `GET /api/admin/users` - List all users with pagination/filters
|
||||
- `GET /api/admin/users/:auth0Sub` - Get single user details
|
||||
- `GET /api/admin/users/:auth0Sub/vehicles` - Get user's vehicles (admin view)
|
||||
- `PATCH /api/admin/users/:auth0Sub/tier` - Update subscription tier
|
||||
- `PATCH /api/admin/users/:auth0Sub/deactivate` - Deactivate user
|
||||
- `PATCH /api/admin/users/:auth0Sub/reactivate` - Reactivate user
|
||||
- `PATCH /api/admin/users/:auth0Sub/profile` - Update user profile
|
||||
- `PATCH /api/admin/users/:auth0Sub/promote` - Promote to admin
|
||||
- `DELETE /api/admin/users/:auth0Sub` - Hard delete user (GDPR)
|
||||
|
||||
**User Vehicles Endpoint:**
|
||||
```
|
||||
GET /api/admin/users/:auth0Sub/vehicles
|
||||
```
|
||||
|
||||
Returns minimal vehicle data for privacy (Year/Make/Model only):
|
||||
```json
|
||||
{
|
||||
"vehicles": [
|
||||
{ "year": 2022, "make": "Toyota", "model": "Camry" },
|
||||
{ "year": 2020, "make": "Honda", "model": "Civic" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Management APIs
|
||||
|
||||
Provides:
|
||||
- `GET /api/admin/admins` - List all admins
|
||||
- `POST /api/admin/admins` - Add admin
|
||||
- `PATCH /api/admin/admins/:auth0Sub/revoke` - Revoke admin
|
||||
- `PATCH /api/admin/admins/:auth0Sub/reinstate` - Reinstate admin
|
||||
- `GET /api/admin/audit-logs` - View audit trail
|
||||
|
||||
### Phase 3: Platform Catalog CRUD (COMPLETED)
|
||||
|
||||
|
||||
@@ -102,6 +102,16 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
handler: adminController.bulkReinstateAdmins.bind(adminController)
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Admin Stats endpoint (dashboard widgets)
|
||||
// ============================================
|
||||
|
||||
// GET /api/admin/stats - Get admin dashboard stats (total users, total vehicles)
|
||||
fastify.get('/admin/stats', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: usersController.getAdminStats.bind(usersController)
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// User Management endpoints (subscription tiers, deactivation)
|
||||
// ============================================
|
||||
@@ -118,6 +128,12 @@ export const adminRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
handler: usersController.getUser.bind(usersController)
|
||||
});
|
||||
|
||||
// GET /api/admin/users/:auth0Sub/vehicles - Get user's vehicles (admin view)
|
||||
fastify.get<{ Params: UserAuth0SubInput }>('/admin/users/:auth0Sub/vehicles', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
handler: usersController.getUserVehicles.bind(usersController)
|
||||
});
|
||||
|
||||
// PATCH /api/admin/users/:auth0Sub/tier - Update subscription tier
|
||||
fastify.patch<{ Params: UserAuth0SubInput; Body: UpdateTierInput }>('/admin/users/:auth0Sub/tier', {
|
||||
preHandler: [fastify.requireAdmin],
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user