fix: improve GPS enrollment and simplify Auth0 SSO
- Remove dashes from device identifiers for better compatibility - Auto-enable consent on enrollment (HR handles consent at hiring) - Remove consent checks from location queries and UI - Simplify Traccar Admin to use Auth0 SSO directly - Fix server URL to return base Traccar URL (app handles port) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -111,7 +111,6 @@ export class GpsService implements OnModuleInit {
|
|||||||
const activeDrivers = await this.prisma.gpsDevice.count({
|
const activeDrivers = await this.prisma.gpsDevice.count({
|
||||||
where: {
|
where: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
consentGiven: true,
|
|
||||||
lastActive: {
|
lastActive: {
|
||||||
gte: new Date(Date.now() - 5 * 60 * 1000), // Active in last 5 minutes
|
gte: new Date(Date.now() - 5 * 60 * 1000), // Active in last 5 minutes
|
||||||
},
|
},
|
||||||
@@ -142,7 +141,6 @@ export class GpsService implements OnModuleInit {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
deviceIdentifier: string;
|
deviceIdentifier: string;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
port: number;
|
|
||||||
instructions: string;
|
instructions: string;
|
||||||
signalMessageSent?: boolean;
|
signalMessageSent?: boolean;
|
||||||
}> {
|
}> {
|
||||||
@@ -164,8 +162,8 @@ export class GpsService implements OnModuleInit {
|
|||||||
throw new BadRequestException('Driver is already enrolled for GPS tracking');
|
throw new BadRequestException('Driver is already enrolled for GPS tracking');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique device identifier
|
// Generate unique device identifier (no special characters for better compatibility)
|
||||||
const deviceIdentifier = `vip-driver-${driverId.slice(0, 8)}`;
|
const deviceIdentifier = `vipdriver${driverId.replace(/-/g, '').slice(0, 8)}`;
|
||||||
|
|
||||||
// Create device in Traccar
|
// Create device in Traccar
|
||||||
const traccarDevice = await this.traccarClient.createDevice(
|
const traccarDevice = await this.traccarClient.createDevice(
|
||||||
@@ -174,13 +172,14 @@ export class GpsService implements OnModuleInit {
|
|||||||
driver.phone || undefined,
|
driver.phone || undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create GPS device record
|
// Create GPS device record (consent pre-approved by HR at hiring)
|
||||||
await this.prisma.gpsDevice.create({
|
await this.prisma.gpsDevice.create({
|
||||||
data: {
|
data: {
|
||||||
driverId,
|
driverId,
|
||||||
traccarDeviceId: traccarDevice.id,
|
traccarDeviceId: traccarDevice.id,
|
||||||
deviceIdentifier,
|
deviceIdentifier,
|
||||||
consentGiven: false,
|
consentGiven: true,
|
||||||
|
consentGivenAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,7 +228,6 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
|
|||||||
success: true,
|
success: true,
|
||||||
deviceIdentifier,
|
deviceIdentifier,
|
||||||
serverUrl,
|
serverUrl,
|
||||||
port: 5055,
|
|
||||||
instructions,
|
instructions,
|
||||||
signalMessageSent,
|
signalMessageSent,
|
||||||
};
|
};
|
||||||
@@ -316,7 +314,6 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
|
|||||||
const devices = await this.prisma.gpsDevice.findMany({
|
const devices = await this.prisma.gpsDevice.findMany({
|
||||||
where: {
|
where: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
consentGiven: true,
|
|
||||||
driver: {
|
driver: {
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
@@ -547,7 +544,6 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
|
|||||||
const devices = await this.prisma.gpsDevice.findMany({
|
const devices = await this.prisma.gpsDevice.findMany({
|
||||||
where: {
|
where: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
consentGiven: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -689,7 +685,8 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get auto-login URL for Traccar (for admin users)
|
* Get Traccar admin URL (Auth0 SSO handles authentication)
|
||||||
|
* User's Auth0 role determines admin status in Traccar
|
||||||
*/
|
*/
|
||||||
async getTraccarAutoLoginUrl(user: User): Promise<{
|
async getTraccarAutoLoginUrl(user: User): Promise<{
|
||||||
url: string;
|
url: string;
|
||||||
@@ -699,36 +696,29 @@ Note: GPS tracking is only active during shift hours (${settings.shiftStartHour}
|
|||||||
throw new BadRequestException('Only administrators can access Traccar admin');
|
throw new BadRequestException('Only administrators can access Traccar admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user is synced to Traccar (this also sets up their token)
|
// Just return the Traccar URL - Auth0 SSO handles authentication
|
||||||
await this.syncUserToTraccar(user);
|
// User must have ADMINISTRATOR role in Auth0 to get admin access in Traccar
|
||||||
|
|
||||||
// Get the token for auto-login
|
|
||||||
const token = this.generateTraccarToken(user.id);
|
|
||||||
const baseUrl = this.traccarClient.getTraccarUrl();
|
const baseUrl = this.traccarClient.getTraccarUrl();
|
||||||
|
|
||||||
// Return URL with token parameter for auto-login
|
|
||||||
// Traccar supports ?token=xxx for direct authentication
|
|
||||||
return {
|
return {
|
||||||
url: `${baseUrl}?token=${token}`,
|
url: baseUrl,
|
||||||
directAccess: true,
|
directAccess: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Traccar session cookie for a user (for proxy/iframe auth)
|
* Get Traccar session cookie for a user (for proxy/iframe auth)
|
||||||
|
* Note: With Auth0 SSO (openid.force=true), this won't work.
|
||||||
|
* Use getTraccarAutoLoginUrl() instead for direct redirect.
|
||||||
*/
|
*/
|
||||||
async getTraccarSessionForUser(user: User): Promise<string | null> {
|
async getTraccarSessionForUser(user: User): Promise<string | null> {
|
||||||
if (user.role !== 'ADMINISTRATOR') {
|
if (user.role !== 'ADMINISTRATOR') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user is synced
|
// With Auth0 SSO, session creation via password is disabled
|
||||||
await this.syncUserToTraccar(user);
|
// Return null to indicate direct access via URL is needed
|
||||||
|
return null;
|
||||||
const password = this.generateTraccarPassword(user.id);
|
|
||||||
const session = await this.traccarClient.createUserSession(user.email, password);
|
|
||||||
|
|
||||||
return session?.cookie || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -342,21 +342,12 @@ export class TraccarClientService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the device port URL for mobile app configuration
|
* Get the device server URL for mobile app configuration
|
||||||
* Returns full HTTPS URL - nginx terminates SSL on port 5055
|
* Returns the Traccar URL (nginx handles SSL termination and proxies to Traccar)
|
||||||
*/
|
*/
|
||||||
getDeviceServerUrl(): string {
|
getDeviceServerUrl(): string {
|
||||||
const port = this.configService.get<string>('TRACCAR_DEVICE_PORT') || '5055';
|
// Return the base Traccar URL - nginx handles routing OsmAnd protocol
|
||||||
|
return this.getTraccarUrl();
|
||||||
// Use the Traccar public URL to derive the device server hostname
|
|
||||||
const traccarUrl = this.getTraccarUrl();
|
|
||||||
try {
|
|
||||||
const url = new URL(traccarUrl);
|
|
||||||
// Return HTTPS URL - nginx terminates SSL and proxies to Traccar
|
|
||||||
return `https://${url.hostname}:${port}`;
|
|
||||||
} catch {
|
|
||||||
return `http://localhost:${port}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -462,7 +462,6 @@ export function GpsTracking() {
|
|||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Driver</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Driver</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Device ID</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Device ID</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Status</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Status</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Consent</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Last Active</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Last Active</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -484,13 +483,6 @@ export function GpsTracking() {
|
|||||||
{device.isActive ? 'Active' : 'Inactive'}
|
{device.isActive ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
|
||||||
{device.consentGiven ? (
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-5 w-5 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||||
{device.lastActive ? formatDistanceToNow(new Date(device.lastActive), { addSuffix: true }) : 'Never'}
|
{device.lastActive ? formatDistanceToNow(new Date(device.lastActive), { addSuffix: true }) : 'Never'}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export interface EnrollmentResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
deviceIdentifier: string;
|
deviceIdentifier: string;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
port: number;
|
|
||||||
instructions: string;
|
instructions: string;
|
||||||
signalMessageSent?: boolean;
|
signalMessageSent?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user