Files
motovaultpro/docs/ADMIN.md
Eric Gullickson 8174e0d5f9 Admin User v1
2025-11-05 19:04:06 -06:00

14 KiB

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

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

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

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:

// 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:

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

# 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:

# 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/

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/

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__/

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:

# 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