Files
vip-coordinator/scripts/setup-auth0-traccar.js
kyle 5ded039793 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>
2026-02-03 18:13:17 +01:00

383 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* Auth0 Setup Script for Traccar GPS Integration
*
* This script sets up Auth0 roles and actions needed for Traccar GPS tracking
* to work with OpenID Connect authentication.
*
* Usage:
* node setup-auth0-traccar.js --token=<AUTH0_TOKEN> --domain=<AUTH0_DOMAIN> --traccar-url=<URL> --admins=<emails>
*
* Or with environment variables:
* AUTH0_MANAGEMENT_TOKEN=<token> \
* AUTH0_DOMAIN=<domain> \
* TRACCAR_URL=<url> \
* ADMIN_EMAILS=<comma-separated-emails> \
* node setup-auth0-traccar.js
*
* Examples:
* node setup-auth0-traccar.js \
* --token=eyJ... \
* --domain=my-tenant.us.auth0.com \
* --traccar-url=https://traccar.myapp.com \
* --admins=admin@example.com,backup-admin@example.com
*
* What this script does:
* 1. Creates ADMINISTRATOR and COORDINATOR roles in Auth0
* 2. Creates a Post Login Action that adds roles to tokens as "groups"
* 3. Deploys the action to the Login flow
* 4. Assigns ADMINISTRATOR role to specified users (if they exist in Auth0)
*
* After running this script:
* - Users with ADMINISTRATOR role get admin access to Traccar
* - Users with COORDINATOR role get standard access to Traccar
* - Users without either role cannot access Traccar
* - Manage roles in Auth0 Dashboard → User Management → Users → [user] → Roles
*/
// Parse command line arguments
function parseArgs() {
const args = {};
process.argv.slice(2).forEach(arg => {
if (arg.startsWith('--')) {
const [key, value] = arg.slice(2).split('=');
args[key] = value;
}
});
return args;
}
const args = parseArgs();
// Configuration - from args, env vars, or defaults
const AUTH0_DOMAIN = args.domain || process.env.AUTH0_DOMAIN || '';
const TRACCAR_URL = args['traccar-url'] || process.env.TRACCAR_URL || '';
const TRACCAR_NAMESPACE = TRACCAR_URL; // Namespace matches the Traccar URL
// Users to assign ADMINISTRATOR role to (comma-separated)
const ADMIN_EMAILS = (args.admins || process.env.ADMIN_EMAILS || '')
.split(',')
.map(e => e.trim())
.filter(e => e.length > 0);
async function main() {
const token = args.token || process.env.AUTH0_MANAGEMENT_TOKEN;
// Validate required parameters
const missing = [];
if (!token) missing.push('--token or AUTH0_MANAGEMENT_TOKEN');
if (!AUTH0_DOMAIN) missing.push('--domain or AUTH0_DOMAIN');
if (!TRACCAR_URL) missing.push('--traccar-url or TRACCAR_URL');
if (ADMIN_EMAILS.length === 0) missing.push('--admins or ADMIN_EMAILS');
if (missing.length > 0) {
console.error('Missing required parameters:');
missing.forEach(m => console.error(` - ${m}`));
console.error('\nUsage:');
console.error(' node setup-auth0-traccar.js \\');
console.error(' --token=<AUTH0_MANAGEMENT_TOKEN> \\');
console.error(' --domain=<your-tenant.us.auth0.com> \\');
console.error(' --traccar-url=<https://traccar.yourdomain.com> \\');
console.error(' --admins=<admin@example.com,other@example.com>');
console.error('\nExample:');
console.error(' node setup-auth0-traccar.js \\');
console.error(' --token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... \\');
console.error(' --domain=my-company.us.auth0.com \\');
console.error(' --traccar-url=https://traccar.myapp.com \\');
console.error(' --admins=john@company.com,jane@company.com');
process.exit(1);
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
console.log('=== Auth0 Traccar Setup ===\n');
console.log(`Auth0 Domain: ${AUTH0_DOMAIN}`);
console.log(`Traccar URL: ${TRACCAR_URL}`);
console.log(`Admin Emails: ${ADMIN_EMAILS.join(', ')}\n`);
// Step 1: Create roles
console.log('1. Creating roles...');
const adminRole = await createRole(headers, {
name: 'ADMINISTRATOR',
description: 'Full admin access to VIP Coordinator and Traccar GPS tracking'
});
console.log(` - ADMINISTRATOR role: ${adminRole ? 'created' : 'already exists'}`);
const coordRole = await createRole(headers, {
name: 'COORDINATOR',
description: 'Coordinator access to VIP Coordinator and Traccar GPS tracking'
});
console.log(` - COORDINATOR role: ${coordRole ? 'created' : 'already exists'}`);
// Get role IDs
const roles = await getRoles(headers);
const adminRoleId = roles.find(r => r.name === 'ADMINISTRATOR')?.id;
const coordRoleId = roles.find(r => r.name === 'COORDINATOR')?.id;
console.log(` Admin Role ID: ${adminRoleId}`);
console.log(` Coordinator Role ID: ${coordRoleId}`);
// Step 2: Create the Post Login Action
console.log('\n2. Creating Post Login Action...');
const actionCode = `
exports.onExecutePostLogin = async (event, api) => {
const namespace = '${TRACCAR_NAMESPACE}';
if (event.authorization && event.authorization.roles) {
// Add roles as "groups" claim (what Traccar expects)
api.idToken.setCustomClaim(namespace + '/groups', event.authorization.roles);
api.accessToken.setCustomClaim(namespace + '/groups', event.authorization.roles);
}
};
`;
const action = await createOrUpdateAction(headers, {
name: 'Add Roles to Traccar Groups',
code: actionCode.trim(),
supported_triggers: [{ id: 'post-login', version: 'v3' }],
runtime: 'node18'
});
if (action) {
console.log(` - Action created: ${action.id}`);
// Deploy the action
console.log('\n3. Deploying action...');
await deployAction(headers, action.id);
console.log(' - Action deployed');
// Add to login flow
console.log('\n4. Adding action to Login flow...');
await addActionToFlow(headers, action.id);
console.log(' - Action added to flow');
} else {
console.log(' - Action already exists or failed to create');
}
// Step 3: Assign ADMINISTRATOR role to admin users
console.log('\n5. Assigning ADMINISTRATOR role to admin users...');
for (const email of ADMIN_EMAILS) {
const user = await findUserByEmail(headers, email);
if (user) {
await assignRoleToUser(headers, user.user_id, adminRoleId);
console.log(` - ${email}: ADMINISTRATOR role assigned`);
} else {
console.log(` - ${email}: User not found (will get role on first login)`);
}
}
console.log('\n=== Setup Complete ===');
console.log('\nNext steps:');
console.log('1. Update Traccar config with these settings:');
console.log(` <entry key="openid.group">${TRACCAR_NAMESPACE}/groups</entry>`);
console.log(' <entry key="openid.adminGroup">ADMINISTRATOR</entry>');
console.log(' <entry key="openid.allowGroup">ADMINISTRATOR,COORDINATOR</entry>');
console.log('\n2. Restart Traccar container');
console.log('\n3. Test login with an admin user');
}
async function createRole(headers, roleData) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/roles`, {
method: 'POST',
headers,
body: JSON.stringify(roleData)
});
if (response.status === 409) {
// Role already exists
return null;
}
if (!response.ok) {
const error = await response.text();
console.error(`Failed to create role ${roleData.name}:`, error);
return null;
}
return await response.json();
} catch (error) {
console.error(`Error creating role ${roleData.name}:`, error.message);
return null;
}
}
async function getRoles(headers) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/roles`, {
method: 'GET',
headers
});
if (!response.ok) {
console.error('Failed to get roles');
return [];
}
return await response.json();
} catch (error) {
console.error('Error getting roles:', error.message);
return [];
}
}
async function createOrUpdateAction(headers, actionData) {
try {
// Check if action exists
const listResponse = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/actions?actionName=${encodeURIComponent(actionData.name)}`, {
method: 'GET',
headers
});
if (listResponse.ok) {
const actions = await listResponse.json();
const existing = actions.actions?.find(a => a.name === actionData.name);
if (existing) {
// Update existing action
const updateResponse = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/actions/${existing.id}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ code: actionData.code })
});
if (updateResponse.ok) {
return await updateResponse.json();
}
}
}
// Create new action
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/actions`, {
method: 'POST',
headers,
body: JSON.stringify(actionData)
});
if (!response.ok) {
const error = await response.text();
console.error('Failed to create action:', error);
return null;
}
return await response.json();
} catch (error) {
console.error('Error creating action:', error.message);
return null;
}
}
async function deployAction(headers, actionId) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/actions/${actionId}/deploy`, {
method: 'POST',
headers
});
if (!response.ok) {
const error = await response.text();
console.error('Failed to deploy action:', error);
return false;
}
return true;
} catch (error) {
console.error('Error deploying action:', error.message);
return false;
}
}
async function addActionToFlow(headers, actionId) {
try {
// Get current flow bindings
const getResponse = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/triggers/post-login/bindings`, {
method: 'GET',
headers
});
if (!getResponse.ok) {
console.error('Failed to get flow bindings');
return false;
}
const currentBindings = await getResponse.json();
// Check if action is already in flow
const alreadyBound = currentBindings.bindings?.some(b => b.action?.id === actionId);
if (alreadyBound) {
console.log(' - Action already in flow');
return true;
}
// Add action to flow
const newBindings = [
...(currentBindings.bindings || []).map(b => ({ ref: { type: 'action_id', value: b.action.id } })),
{ ref: { type: 'action_id', value: actionId } }
];
const updateResponse = await fetch(`https://${AUTH0_DOMAIN}/api/v2/actions/triggers/post-login/bindings`, {
method: 'PATCH',
headers,
body: JSON.stringify({ bindings: newBindings })
});
if (!updateResponse.ok) {
const error = await updateResponse.text();
console.error('Failed to update flow bindings:', error);
return false;
}
return true;
} catch (error) {
console.error('Error adding action to flow:', error.message);
return false;
}
}
async function findUserByEmail(headers, email) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/users-by-email?email=${encodeURIComponent(email)}`, {
method: 'GET',
headers
});
if (!response.ok) {
return null;
}
const users = await response.json();
return users[0] || null;
} catch (error) {
console.error(`Error finding user ${email}:`, error.message);
return null;
}
}
async function assignRoleToUser(headers, userId, roleId) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/api/v2/users/${encodeURIComponent(userId)}/roles`, {
method: 'POST',
headers,
body: JSON.stringify({ roles: [roleId] })
});
if (!response.ok && response.status !== 204) {
const error = await response.text();
console.error(`Failed to assign role to user:`, error);
return false;
}
return true;
} catch (error) {
console.error('Error assigning role:', error.message);
return false;
}
}
main().catch(console.error);