Admin User v1
This commit is contained in:
600
docs/ADMIN.md
Normal file
600
docs/ADMIN.md
Normal 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`
|
||||
Reference in New Issue
Block a user