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