feat: add GPS tracking with Traccar integration

- Add GPS module with Traccar client service for device management
- Add driver enrollment flow with QR code generation
- Add real-time location tracking on driver profiles
- Add GPS settings configuration in admin tools
- Add Auth0 OpenID Connect setup script for Traccar
- Add deployment configs for production server
- Update nginx configs for SSL on GPS port 5055
- Add timezone setting support
- Various UI improvements and bug fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 18:13:17 +01:00
parent 3814d175ff
commit 5ded039793
91 changed files with 4403 additions and 68 deletions

View File

@@ -0,0 +1,335 @@
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { GpsService } from './gps.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Role } from '@prisma/client';
import { EnrollDriverDto, ConfirmConsentDto } from './dto/enroll-driver.dto';
import { UpdateGpsSettingsDto } from './dto/update-gps-settings.dto';
import { PrismaService } from '../prisma/prisma.service';
@Controller('gps')
@UseGuards(JwtAuthGuard, RolesGuard)
export class GpsController {
constructor(
private readonly gpsService: GpsService,
private readonly prisma: PrismaService,
) {}
// ============================================
// Admin-only endpoints
// ============================================
/**
* Get GPS system status
*/
@Get('status')
@Roles(Role.ADMINISTRATOR)
async getStatus() {
return this.gpsService.getStatus();
}
/**
* Get GPS settings
*/
@Get('settings')
@Roles(Role.ADMINISTRATOR)
async getSettings() {
const settings = await this.gpsService.getSettings();
// Don't return the password
return {
...settings,
traccarAdminPassword: settings.traccarAdminPassword ? '********' : null,
};
}
/**
* Update GPS settings
*/
@Patch('settings')
@Roles(Role.ADMINISTRATOR)
async updateSettings(@Body() dto: UpdateGpsSettingsDto) {
const settings = await this.gpsService.updateSettings(dto);
return {
...settings,
traccarAdminPassword: settings.traccarAdminPassword ? '********' : null,
};
}
/**
* Get all enrolled devices
*/
@Get('devices')
@Roles(Role.ADMINISTRATOR)
async getEnrolledDevices() {
return this.gpsService.getEnrolledDevices();
}
/**
* Enroll a driver for GPS tracking
*/
@Post('enroll/:driverId')
@Roles(Role.ADMINISTRATOR)
async enrollDriver(
@Param('driverId') driverId: string,
@Body() dto: EnrollDriverDto,
) {
return this.gpsService.enrollDriver(driverId, dto.sendSignalMessage ?? true);
}
/**
* Unenroll a driver from GPS tracking
*/
@Delete('devices/:driverId')
@Roles(Role.ADMINISTRATOR)
async unenrollDriver(@Param('driverId') driverId: string) {
return this.gpsService.unenrollDriver(driverId);
}
/**
* Get all active driver locations (Admin map view)
*/
@Get('locations')
@Roles(Role.ADMINISTRATOR)
async getActiveDriverLocations() {
return this.gpsService.getActiveDriverLocations();
}
/**
* Get a specific driver's location
*/
@Get('locations/:driverId')
@Roles(Role.ADMINISTRATOR)
async getDriverLocation(@Param('driverId') driverId: string) {
const location = await this.gpsService.getDriverLocation(driverId);
if (!location) {
throw new NotFoundException('Driver not found or not enrolled for GPS tracking');
}
return location;
}
/**
* Get a driver's stats (Admin viewing any driver)
*/
@Get('stats/:driverId')
@Roles(Role.ADMINISTRATOR)
async getDriverStats(
@Param('driverId') driverId: string,
@Query('from') fromStr?: string,
@Query('to') toStr?: string,
) {
const from = fromStr ? new Date(fromStr) : undefined;
const to = toStr ? new Date(toStr) : undefined;
return this.gpsService.getDriverStats(driverId, from, to);
}
// ============================================
// Traccar Admin Access
// ============================================
/**
* Check Traccar setup status
*/
@Get('traccar/status')
@Roles(Role.ADMINISTRATOR)
async getTraccarSetupStatus() {
return this.gpsService.checkTraccarSetup();
}
/**
* Perform initial Traccar setup
*/
@Post('traccar/setup')
@Roles(Role.ADMINISTRATOR)
async performTraccarSetup(@CurrentUser() user: any) {
const success = await this.gpsService.performTraccarSetup(user.email);
if (!success) {
throw new NotFoundException('Failed to setup Traccar. It may already be configured.');
}
return { success: true, message: 'Traccar setup complete' };
}
/**
* Sync all VIP admins to Traccar
*/
@Post('traccar/sync-admins')
@Roles(Role.ADMINISTRATOR)
async syncAdminsToTraccar() {
return this.gpsService.syncAllAdminsToTraccar();
}
/**
* Get Traccar admin URL (auto-login for current user)
*/
@Get('traccar/admin-url')
@Roles(Role.ADMINISTRATOR)
async getTraccarAdminUrl(@CurrentUser() user: any) {
// Get full user from database
const fullUser = await this.prisma.user.findUnique({
where: { id: user.id },
});
if (!fullUser) {
throw new NotFoundException('User not found');
}
return this.gpsService.getTraccarAutoLoginUrl(fullUser);
}
/**
* Get Traccar session for iframe/proxy access
*/
@Get('traccar/session')
@Roles(Role.ADMINISTRATOR)
async getTraccarSession(@CurrentUser() user: any) {
const fullUser = await this.prisma.user.findUnique({
where: { id: user.id },
});
if (!fullUser) {
throw new NotFoundException('User not found');
}
const session = await this.gpsService.getTraccarSessionForUser(fullUser);
if (!session) {
throw new NotFoundException('Could not create Traccar session');
}
return { session };
}
// ============================================
// Driver self-service endpoints
// ============================================
/**
* Get my GPS enrollment status
*/
@Get('me')
@Roles(Role.DRIVER, Role.ADMINISTRATOR, Role.COORDINATOR)
async getMyGpsStatus(@CurrentUser() user: any) {
// Find driver linked to this user
const driver = await this.prisma.driver.findFirst({
where: {
userId: user.id,
deletedAt: null,
},
include: {
gpsDevice: true,
},
});
if (!driver) {
return { enrolled: false, message: 'No driver profile linked to your account' };
}
if (!driver.gpsDevice) {
return { enrolled: false, driverId: driver.id };
}
return {
enrolled: true,
driverId: driver.id,
deviceIdentifier: driver.gpsDevice.deviceIdentifier,
consentGiven: driver.gpsDevice.consentGiven,
consentGivenAt: driver.gpsDevice.consentGivenAt,
isActive: driver.gpsDevice.isActive,
lastActive: driver.gpsDevice.lastActive,
};
}
/**
* Confirm GPS tracking consent (Driver accepting tracking)
*/
@Post('me/consent')
@Roles(Role.DRIVER)
async confirmMyConsent(
@CurrentUser() user: any,
@Body() dto: ConfirmConsentDto,
) {
const driver = await this.prisma.driver.findFirst({
where: {
userId: user.id,
deletedAt: null,
},
});
if (!driver) {
throw new NotFoundException('No driver profile linked to your account');
}
await this.gpsService.confirmConsent(driver.id, dto.consentGiven);
return {
success: true,
message: dto.consentGiven
? 'GPS tracking consent confirmed. Your location will be tracked during shift hours.'
: 'GPS tracking consent revoked. Your location will not be tracked.',
};
}
/**
* Get my GPS stats (Driver viewing own stats)
*/
@Get('me/stats')
@Roles(Role.DRIVER, Role.ADMINISTRATOR, Role.COORDINATOR)
async getMyStats(
@CurrentUser() user: any,
@Query('from') fromStr?: string,
@Query('to') toStr?: string,
) {
const driver = await this.prisma.driver.findFirst({
where: {
userId: user.id,
deletedAt: null,
},
});
if (!driver) {
throw new NotFoundException('No driver profile linked to your account');
}
const from = fromStr ? new Date(fromStr) : undefined;
const to = toStr ? new Date(toStr) : undefined;
return this.gpsService.getDriverStats(driver.id, from, to);
}
/**
* Get my current location
*/
@Get('me/location')
@Roles(Role.DRIVER, Role.ADMINISTRATOR, Role.COORDINATOR)
async getMyLocation(@CurrentUser() user: any) {
const driver = await this.prisma.driver.findFirst({
where: {
userId: user.id,
deletedAt: null,
},
});
if (!driver) {
throw new NotFoundException('No driver profile linked to your account');
}
const location = await this.gpsService.getDriverLocation(driver.id);
if (!location) {
throw new NotFoundException('You are not enrolled for GPS tracking');
}
return location;
}
}