- 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>
383 lines
12 KiB
JavaScript
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);
|