Admin User v1

This commit is contained in:
Eric Gullickson
2025-11-05 19:04:06 -06:00
parent e4e7e32a4f
commit 8174e0d5f9
48 changed files with 11289 additions and 1112 deletions

600
docs/ADMIN.md Normal file
View File

@@ -0,0 +1,600 @@
# Admin Feature Documentation
Complete reference for the admin role management, authorization, and cross-tenant oversight capabilities in MotoVaultPro.
## Overview
The admin feature provides role-based access control for system administrators to manage:
- Admin user accounts (create, revoke, reinstate)
- Vehicle catalog data (makes, models, years, trims, engines)
- Gas stations and user favorites
- Complete audit trail of all admin actions
## Architecture
### Backend Feature Capsule
Location: `backend/src/features/admin/`
Structure:
```
admin/
├── api/
│ ├── admin.controller.ts - HTTP handlers for admin management
│ ├── admin.routes.ts - Route registration
│ ├── admin.validation.ts - Input validation schemas
│ ├── catalog.controller.ts - Vehicle catalog handlers
│ └── stations.controller.ts - Station oversight handlers
├── domain/
│ ├── admin.types.ts - TypeScript type definitions
│ ├── admin.service.ts - Admin user management logic
│ ├── vehicle-catalog.service.ts - Catalog CRUD logic
│ └── station-oversight.service.ts - Station management logic
├── data/
│ └── admin.repository.ts - Database access layer
├── migrations/
│ ├── 001_create_admin_users.sql - Admin tables and seed
│ └── 002_create_platform_change_log.sql - Catalog audit log
└── tests/
├── unit/ - Service and guard tests
├── integration/ - Full API endpoint tests
└── fixtures/ - Test data
```
### Core Plugins
- **auth.plugin.ts**: Enhanced with `request.userContext` containing `userId`, `email`, `isAdmin`, `adminRecord`
- **admin-guard.plugin.ts**: `fastify.requireAdmin` preHandler that checks `admin_users` table and enforces 403 on non-admins
### Frontend Feature
Location: `frontend/src/features/admin/`
Structure:
```
admin/
├── types/admin.types.ts - TypeScript types (mirroring backend)
├── api/admin.api.ts - API client functions
├── hooks/
│ ├── useAdminAccess.ts - Verify admin status
│ ├── useAdmins.ts - Admin user management
│ ├── useCatalog.ts - Vehicle catalog
│ └── useStationOverview.ts - Station management
├── pages/
│ ├── AdminUsersPage.tsx - Desktop user management
│ ├── AdminCatalogPage.tsx - Desktop catalog management
│ └── AdminStationsPage.tsx - Desktop station management
├── mobile/
│ ├── AdminUsersMobileScreen.tsx - Mobile user management
│ ├── AdminCatalogMobileScreen.tsx - Mobile catalog management
│ └── AdminStationsMobileScreen.tsx - Mobile station management
└── __tests__/ - Component and hook tests
```
## Database Schema
### admin_users table
```sql
CREATE TABLE admin_users (
auth0_sub VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(50) NOT NULL DEFAULT 'admin',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255) NOT NULL,
revoked_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
**Indexes:**
- `auth0_sub` (PRIMARY KEY) - OAuth ID from Auth0
- `email` - For admin lookups by email
- `created_at` - For audit trails
- `revoked_at` - For active admin queries
### admin_audit_logs table
```sql
CREATE TABLE admin_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
actor_admin_id VARCHAR(255) NOT NULL,
target_admin_id VARCHAR(255),
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(100),
resource_id VARCHAR(255),
context JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
**Actions logged:**
- CREATE - New admin or resource created
- UPDATE - Resource updated
- DELETE - Resource deleted
- REVOKE - Admin access revoked
- REINSTATE - Admin access restored
- VIEW - Data accessed (for sensitive operations)
### platform_change_log table
```sql
CREATE TABLE platform_change_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
change_type VARCHAR(50) NOT NULL,
resource_type VARCHAR(100) NOT NULL,
resource_id VARCHAR(255),
old_value JSONB,
new_value JSONB,
changed_by VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
**Resource types:**
- makes, models, years, trims, engines
- stations
- users
## API Reference
### Phase 2: Admin Management
#### List all admins
```
GET /api/admin/admins
Authorization: Bearer <JWT>
Guard: fastify.requireAdmin
Response (200):
{
"total": 2,
"admins": [
{
"auth0Sub": "auth0|admin1",
"email": "admin@motovaultpro.com",
"role": "admin",
"createdAt": "2024-01-01T00:00:00Z",
"createdBy": "system",
"revokedAt": null,
"updatedAt": "2024-01-01T00:00:00Z"
}
]
}
```
#### Create admin
```
POST /api/admin/admins
Authorization: Bearer <JWT>
Guard: fastify.requireAdmin
Content-Type: application/json
Request:
{
"email": "newadmin@example.com",
"role": "admin"
}
Response (201):
{
"auth0Sub": "auth0|newadmin",
"email": "newadmin@example.com",
"role": "admin",
"createdAt": "2024-01-15T10:30:00Z",
"createdBy": "auth0|existing",
"revokedAt": null,
"updatedAt": "2024-01-15T10:30:00Z"
}
Audit log entry:
{
"actor_admin_id": "auth0|existing",
"target_admin_id": "auth0|newadmin",
"action": "CREATE",
"resource_type": "admin_user",
"resource_id": "newadmin@example.com",
"context": { "email": "newadmin@example.com", "role": "admin" }
}
```
#### Revoke admin
```
PATCH /api/admin/admins/:auth0Sub/revoke
Authorization: Bearer <JWT>
Guard: fastify.requireAdmin
Response (200):
{
"auth0Sub": "auth0|toadmin",
"email": "admin@motovaultpro.com",
"role": "admin",
"createdAt": "2024-01-01T00:00:00Z",
"createdBy": "system",
"revokedAt": "2024-01-15T10:35:00Z",
"updatedAt": "2024-01-15T10:35:00Z"
}
Errors:
- 400 Bad Request - Last active admin (cannot revoke)
- 403 Forbidden - Not an admin
- 404 Not Found - Admin not found
```
#### Reinstate admin
```
PATCH /api/admin/admins/:auth0Sub/reinstate
Authorization: Bearer <JWT>
Guard: fastify.requireAdmin
Response (200):
{
"auth0Sub": "auth0|toadmin",
"email": "admin@motovaultpro.com",
"role": "admin",
"createdAt": "2024-01-01T00:00:00Z",
"createdBy": "system",
"revokedAt": null,
"updatedAt": "2024-01-15T10:40:00Z"
}
```
#### Audit logs
```
GET /api/admin/audit-logs?limit=100&offset=0
Authorization: Bearer <JWT>
Guard: fastify.requireAdmin
Response (200):
{
"total": 150,
"logs": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"actor_admin_id": "auth0|admin1",
"target_admin_id": "auth0|admin2",
"action": "CREATE",
"resource_type": "admin_user",
"resource_id": "admin2@motovaultpro.com",
"context": { "email": "admin2@motovaultpro.com", "role": "admin" },
"created_at": "2024-01-15T10:30:00Z"
}
]
}
```
### Phase 3: Catalog CRUD
All catalog endpoints follow RESTful patterns:
```
GET /api/admin/catalog/{resource} - List all
GET /api/admin/catalog/{parent}/{parentId}/{resource} - List by parent
POST /api/admin/catalog/{resource} - Create
PUT /api/admin/catalog/{resource}/:id - Update
DELETE /api/admin/catalog/{resource}/:id - Delete
```
**Resources:** makes, models, years, trims, engines
**Example: Get all makes**
```
GET /api/admin/catalog/makes
Guard: fastify.requireAdmin
Response (200):
{
"total": 42,
"makes": [
{ "id": "1", "name": "Toyota", "createdAt": "...", "updatedAt": "..." },
{ "id": "2", "name": "Honda", "createdAt": "...", "updatedAt": "..." }
]
}
```
**Cache invalidation:** All mutations invalidate `platform:*` Redis keys
**Audit trail:** All mutations recorded in `platform_change_log` with old and new values
### Phase 4: Station Oversight
#### List all stations
```
GET /api/admin/stations?limit=100&offset=0&search=query
Guard: fastify.requireAdmin
Response (200):
{
"total": 1250,
"stations": [
{
"id": "station-1",
"placeId": "ChIJxxx",
"name": "Shell Station Downtown",
"address": "123 Main St",
"latitude": 40.7128,
"longitude": -74.0060,
"createdAt": "...",
"deletedAt": null
}
]
}
```
#### Create station
```
POST /api/admin/stations
Guard: fastify.requireAdmin
Content-Type: application/json
Request:
{
"placeId": "ChIJxxx",
"name": "New Station",
"address": "456 Oak Ave",
"latitude": 40.7580,
"longitude": -73.9855
}
Response (201): Station object with all fields
Cache invalidation:
- mvp:stations:* - All station caches
- mvp:stations:search:* - Search result caches
```
#### Delete station (soft or hard)
```
DELETE /api/admin/stations/:stationId?force=false
Guard: fastify.requireAdmin
Query parameters:
- force=false (default) - Soft delete (set deleted_at)
- force=true - Hard delete (permanent removal)
Response (204 No Content)
```
#### User station management
```
GET /api/admin/users/:userId/stations
Guard: fastify.requireAdmin
Response (200):
{
"userId": "auth0|user123",
"stations": [...]
}
```
## Authorization Rules
### Admin Guard
The `fastify.requireAdmin` preHandler enforces:
1. **JWT validation** - User must be authenticated
2. **Admin check** - User must exist in `admin_users` table
3. **Active status** - User's `revoked_at` must be NULL
4. **Error response** - Returns 403 Forbidden with message "Admin access required"
### Last Admin Protection
The system maintains at least one active admin:
- Cannot revoke the last active admin (returns 400 Bad Request)
- Prevents system lockout
- Enforced in `AdminService.revokeAdmin()`
### Audit Trail
All admin actions logged:
- Actor admin ID (who performed action)
- Target admin ID (who was affected, if applicable)
- Action type (CREATE, UPDATE, DELETE, REVOKE, REINSTATE)
- Resource type and ID
- Context (relevant data, like old/new values)
- Timestamp
## Security Considerations
### Input Validation
All inputs validated using Zod schemas:
- Email format and uniqueness
- Role enum validation
- Required field presence
- Type checking
### Parameterized Queries
All database operations use parameterized queries:
```typescript
// Good - Parameterized
const result = await pool.query(
'SELECT * FROM admin_users WHERE email = $1',
[email]
);
// Bad - SQL concatenation (never done)
const result = await pool.query(
`SELECT * FROM admin_users WHERE email = '${email}'`
);
```
### Transaction Support
Catalog mutations wrapped in transactions:
```sql
BEGIN;
-- INSERT/UPDATE/DELETE operations
COMMIT; -- or ROLLBACK on error
```
### Cache Invalidation
Prevents stale data:
- All catalog mutations invalidate `platform:*` keys
- All station mutations invalidate `mvp:stations:*` keys
- User station mutations invalidate `mvp:stations:saved:{userId}`
## Deployment
### Prerequisites
1. **Database migrations** - Run all migrations before deploying
2. **Initial admin** - First admin seeded automatically in migration
3. **Auth0 configuration** - Admin user must exist in Auth0
### Deployment Steps
```bash
# 1. Build containers
make rebuild
# 2. Run migrations (automatically on startup)
docker compose exec mvp-backend npm run migrate
# 3. Verify admin user created
docker compose exec mvp-backend npm run verify-admin
# 4. Check backend health
curl https://motovaultpro.com/api/health
# 5. Verify frontend build
curl https://motovaultpro.com
```
### Rollback
If issues occur:
```bash
# Revoke problematic admin
docker compose exec mvp-backend npm run admin:revoke admin@motovaultpro.com
# Reinstate previous admin
docker compose exec mvp-backend npm run admin:reinstate <auth0_sub>
# Downgrade admin feature (keep data)
docker compose down
git checkout previous-version
make rebuild
make start
```
## Testing
### Backend Unit Tests
Location: `backend/src/features/admin/tests/unit/`
```bash
npm test -- features/admin/tests/unit
```
Tests:
- Admin guard authorization logic
- Admin service business rules
- Repository error handling
- Last admin protection
### Backend Integration Tests
Location: `backend/src/features/admin/tests/integration/`
```bash
npm test -- features/admin/tests/integration
```
Tests:
- Full API endpoints
- Database persistence
- Audit logging
- Admin guard in request context
- CRUD operations
- Cache invalidation
- Permission enforcement
### Frontend Tests
Location: `frontend/src/features/admin/__tests__/`
```bash
docker compose exec mvp-frontend npm test
```
Tests:
- useAdminAccess hook (loading, admin, non-admin, error states)
- Admin page rendering
- Admin route guards
- Navigation
### E2E Testing
1. **Desktop workflow**
- Navigate to `/garage/settings`
- Verify "Admin Console" card visible (if admin)
- Click "User Management"
- Verify admin list loads
- Try to create new admin (if permitted)
2. **Mobile workflow**
- Open app on mobile viewport (375px)
- Navigate to settings
- Verify admin section visible (if admin)
- Tap "Users" button
- Verify admin list loads
## Monitoring & Troubleshooting
### Common Issues
**Issue: "Admin access required" (403 Forbidden)**
- Verify user in `admin_users` table
- Check `revoked_at` is NULL
- Verify JWT token valid
- Check Auth0 configuration
**Issue: Stale catalog data**
- Verify Redis is running
- Check cache invalidation logs
- Manually flush: `redis-cli DEL 'mvp:platform:*'`
**Issue: Audit log not recording**
- Check `admin_audit_logs` table exists
- Verify migrations ran
- Check database connection
### Logs
View admin-related logs:
```bash
# Backend logs
make logs | grep -i admin
# Check specific action
docker compose exec mvp-backend psql -U postgres -d motovaultpro \
-c "SELECT * FROM admin_audit_logs WHERE action = 'CREATE' ORDER BY created_at DESC LIMIT 10;"
# Check revoked admins
docker compose exec mvp-backend psql -U postgres -d motovaultpro \
-c "SELECT email, revoked_at FROM admin_users WHERE revoked_at IS NOT NULL;"
```
## Next Steps
### Planned Enhancements
1. **Role-based permissions** - Extend from binary admin to granular roles (admin, catalog_editor, station_manager)
2. **2FA for admins** - Enhanced security with two-factor authentication
3. **Admin impersonation** - Test user issues as admin without changing password
4. **Bulk operations** - Import/export catalog data
5. **Advanced analytics** - Admin activity dashboards
## References
- Backend feature: `backend/src/features/admin/README.md`
- Frontend feature: `frontend/src/features/admin/` (see individual files)
- Architecture: `docs/PLATFORM-SERVICES.md`
- Testing: `docs/TESTING.md`