feat: add VIP roster tracking and accountability reports
- Add isRosterOnly flag for VIPs who attend but don't need transportation - Add VIP contact fields (phone, email) and emergency contact info - Create Reports page under Admin menu with Accountability Roster - Report shows all VIPs (active + roster-only) with contact/emergency info - Export to CSV functionality for emergency preparedness - VIP list filters roster-only by default with toggle to show - VIP form includes collapsible contact/emergency section - Fix first-user race condition with Serializable transaction - Remove Traccar hardcoded default credentials - Add feature flags endpoint for optional services Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
backend/.env
40
backend/.env
@@ -1,40 +0,0 @@
|
||||
# ============================================
|
||||
# Application Configuration
|
||||
# ============================================
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# ============================================
|
||||
DATABASE_URL="postgresql://postgres:changeme@localhost:5433/vip_coordinator"
|
||||
|
||||
# ============================================
|
||||
# Redis Configuration (Optional)
|
||||
# ============================================
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
# ============================================
|
||||
# Auth0 Configuration
|
||||
# ============================================
|
||||
# Get these from your Auth0 dashboard:
|
||||
# 1. Create Application (Single Page Application)
|
||||
# 2. Create API
|
||||
# 3. Configure callback URLs: http://localhost:5173/callback
|
||||
AUTH0_DOMAIN="dev-s855cy3bvjjbkljt.us.auth0.com"
|
||||
AUTH0_AUDIENCE="https://vip-coordinator-api"
|
||||
AUTH0_ISSUER="https://dev-s855cy3bvjjbkljt.us.auth0.com/"
|
||||
|
||||
# ============================================
|
||||
# Flight Tracking API (Optional)
|
||||
# ============================================
|
||||
# Get API key from: https://aviationstack.com/
|
||||
AVIATIONSTACK_API_KEY="your-aviationstack-api-key"
|
||||
|
||||
# ============================================
|
||||
# AI Copilot Configuration (Optional)
|
||||
# ============================================
|
||||
# Get API key from: https://console.anthropic.com/
|
||||
# Cost: ~$3 per million tokens
|
||||
ANTHROPIC_API_KEY="your-anthropic-api-key"
|
||||
@@ -6,19 +6,19 @@ NODE_ENV=development
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# Database Configuration (required)
|
||||
# ============================================
|
||||
# Port 5433 is used to avoid conflicts with local PostgreSQL
|
||||
DATABASE_URL="postgresql://postgres:changeme@localhost:5433/vip_coordinator"
|
||||
|
||||
# ============================================
|
||||
# Redis Configuration (Optional)
|
||||
# Redis Configuration (required)
|
||||
# ============================================
|
||||
# Port 6380 is used to avoid conflicts with local Redis
|
||||
REDIS_URL="redis://localhost:6380"
|
||||
|
||||
# ============================================
|
||||
# Auth0 Configuration
|
||||
# Auth0 Configuration (required)
|
||||
# ============================================
|
||||
# Get these from your Auth0 dashboard:
|
||||
# 1. Create Application (Single Page Application)
|
||||
@@ -29,6 +29,16 @@ AUTH0_AUDIENCE="https://your-api-identifier"
|
||||
AUTH0_ISSUER="https://your-tenant.us.auth0.com/"
|
||||
|
||||
# ============================================
|
||||
# Flight Tracking API (Optional)
|
||||
# Optional Services
|
||||
# ============================================
|
||||
AVIATIONSTACK_API_KEY="your-aviationstack-api-key"
|
||||
# Leave empty or remove to disable the feature.
|
||||
# The app auto-detects which features are available.
|
||||
|
||||
# Flight tracking API (https://aviationstack.com/)
|
||||
AVIATIONSTACK_API_KEY=
|
||||
|
||||
# AI Copilot (https://console.anthropic.com/)
|
||||
ANTHROPIC_API_KEY=
|
||||
|
||||
# Signal webhook authentication (recommended in production)
|
||||
SIGNAL_WEBHOOK_SECRET=
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "vips" ADD COLUMN "email" TEXT,
|
||||
ADD COLUMN "emergencyContactName" TEXT,
|
||||
ADD COLUMN "emergencyContactPhone" TEXT,
|
||||
ADD COLUMN "isRosterOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "phone" TEXT;
|
||||
@@ -52,6 +52,16 @@ model VIP {
|
||||
venueTransport Boolean @default(false)
|
||||
partySize Int @default(1) // Total people: VIP + entourage
|
||||
notes String? @db.Text
|
||||
|
||||
// Roster-only flag: true = just tracking for accountability, not active coordination
|
||||
isRosterOnly Boolean @default(false)
|
||||
|
||||
// Emergency contact info (for accountability reports)
|
||||
phone String?
|
||||
email String?
|
||||
emergencyContactName String?
|
||||
emergencyContactPhone String?
|
||||
|
||||
flights Flight[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -18,52 +18,56 @@ export class AuthService {
|
||||
const name = payload[`${namespace}/name`] || payload.name || 'Unknown User';
|
||||
const picture = payload[`${namespace}/picture`] || payload.picture;
|
||||
|
||||
// Check if user exists
|
||||
let user = await this.prisma.user.findUnique({
|
||||
where: { auth0Id },
|
||||
// Check if user exists (exclude soft-deleted users)
|
||||
let user = await this.prisma.user.findFirst({
|
||||
where: { auth0Id, deletedAt: null },
|
||||
include: { driver: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// Check if this is the first user (auto-approve as admin)
|
||||
const approvedUserCount = await this.prisma.user.count({
|
||||
where: { isApproved: true, deletedAt: null },
|
||||
});
|
||||
const isFirstUser = approvedUserCount === 0;
|
||||
// Use serializable transaction to prevent race condition
|
||||
// where two simultaneous registrations both become admin
|
||||
user = await this.prisma.$transaction(async (tx) => {
|
||||
const approvedUserCount = await tx.user.count({
|
||||
where: { isApproved: true, deletedAt: null },
|
||||
});
|
||||
const isFirstUser = approvedUserCount === 0;
|
||||
|
||||
this.logger.log(
|
||||
`Creating new user: ${email} (approvedUserCount: ${approvedUserCount}, isFirstUser: ${isFirstUser})`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Creating new user: ${email} (approvedUserCount: ${approvedUserCount}, isFirstUser: ${isFirstUser})`,
|
||||
);
|
||||
|
||||
// Create new user
|
||||
// First user is auto-approved as ADMINISTRATOR
|
||||
// Subsequent users default to DRIVER and require approval
|
||||
user = await this.prisma.user.create({
|
||||
data: {
|
||||
auth0Id,
|
||||
email,
|
||||
name,
|
||||
picture,
|
||||
role: isFirstUser ? Role.ADMINISTRATOR : Role.DRIVER,
|
||||
isApproved: isFirstUser, // Auto-approve first user only
|
||||
},
|
||||
include: { driver: true },
|
||||
});
|
||||
// First user is auto-approved as ADMINISTRATOR
|
||||
// Subsequent users default to DRIVER and require approval
|
||||
const newUser = await tx.user.create({
|
||||
data: {
|
||||
auth0Id,
|
||||
email,
|
||||
name,
|
||||
picture,
|
||||
role: isFirstUser ? Role.ADMINISTRATOR : Role.DRIVER,
|
||||
isApproved: isFirstUser,
|
||||
},
|
||||
include: { driver: true },
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`User created: ${user.email} with role ${user.role} (approved: ${user.isApproved})`,
|
||||
);
|
||||
this.logger.log(
|
||||
`User created: ${newUser.email} with role ${newUser.role} (approved: ${newUser.isApproved})`,
|
||||
);
|
||||
|
||||
return newUser;
|
||||
}, { isolationLevel: 'Serializable' });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
* Get current user profile (excludes soft-deleted users)
|
||||
*/
|
||||
async getCurrentUser(auth0Id: string) {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { auth0Id },
|
||||
return this.prisma.user.findFirst({
|
||||
where: { auth0Id, deletedAt: null },
|
||||
include: { driver: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ export class TraccarClientService implements OnModuleInit {
|
||||
private client: AxiosInstance;
|
||||
private readonly baseUrl: string;
|
||||
private sessionCookie: string | null = null;
|
||||
private adminUser: string = 'admin';
|
||||
private adminPassword: string = 'admin';
|
||||
private adminUser: string = '';
|
||||
private adminPassword: string = '';
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.get<string>('TRACCAR_API_URL') || 'http://localhost:8082';
|
||||
@@ -86,6 +86,11 @@ export class TraccarClientService implements OnModuleInit {
|
||||
* Authenticate with Traccar and get session cookie
|
||||
*/
|
||||
async authenticate(): Promise<boolean> {
|
||||
if (!this.adminUser || !this.adminPassword) {
|
||||
this.logger.warn('Traccar credentials not configured - skipping authentication');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.client.post(
|
||||
'/api/session',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MaxFileSizeValidator,
|
||||
FileTypeValidator,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { UpdatePdfSettingsDto } from './dto/update-pdf-settings.dto';
|
||||
@@ -22,7 +23,24 @@ import { CanUpdate } from '../auth/decorators/check-ability.decorator';
|
||||
@Controller('settings')
|
||||
@UseGuards(JwtAuthGuard, AbilitiesGuard)
|
||||
export class SettingsController {
|
||||
constructor(private readonly settingsService: SettingsService) {}
|
||||
constructor(
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Feature flags - tells the frontend which optional services are configured.
|
||||
* No ability decorator = any authenticated user can access.
|
||||
*/
|
||||
@Get('features')
|
||||
getFeatureFlags() {
|
||||
return {
|
||||
copilot: !!this.configService.get('ANTHROPIC_API_KEY'),
|
||||
flightTracking: !!this.configService.get('AVIATIONSTACK_API_KEY'),
|
||||
signalMessaging: !!this.configService.get('SIGNAL_API_URL'),
|
||||
gpsTracking: !!this.configService.get('TRACCAR_API_URL'),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('pdf')
|
||||
@CanUpdate('Settings') // Admin-only (Settings subject is admin-only)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsEmail,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Department, ArrivalMode } from '@prisma/client';
|
||||
@@ -43,4 +44,27 @@ export class CreateVipDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
|
||||
// Roster-only flag: true = just tracking for accountability, not active coordination
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isRosterOnly?: boolean;
|
||||
|
||||
// VIP contact info
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
phone?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
|
||||
// Emergency contact info (for accountability reports)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
emergencyContactName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
emergencyContactPhone?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user