PSA/server/migrations/20250703193155_add_default_roles_and_permissions.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

691 lines
32 KiB
JavaScript

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function(knex) {
// First, add the description column to permissions table if it doesn't exist
const hasDescriptionColumn = await knex.schema.hasColumn('permissions', 'description');
if (!hasDescriptionColumn) {
await knex.schema.alterTable('permissions', (table) => {
table.text('description');
});
}
// Helper function to create a permission if it doesn't exist
const ensurePermission = async (tenant, resource, action, msp = true, client = false, description = null) => {
const existing = await knex('permissions')
.where({ tenant, resource, action })
.first();
if (!existing) {
const [permission] = await knex('permissions')
.insert({
tenant,
resource,
action,
msp,
client,
description,
permission_id: knex.raw('gen_random_uuid()'),
created_at: knex.fn.now()
})
.returning('*');
return permission;
}
// Update existing permission with msp/client flags and description
await knex('permissions')
.where({ tenant, resource, action })
.update({ msp, client, description });
return existing;
};
// Helper function to create a role if it doesn't exist
const ensureRole = async (tenant, roleName, description, msp = true, client = false) => {
// For roles that can exist in both portals, we need to check for the specific portal
const existing = await knex('roles')
.where({ tenant, role_name: roleName, msp, client })
.first();
if (!existing) {
// Check if role with same name exists in opposite portal
const oppositePortalRole = await knex('roles')
.where({ tenant, role_name: roleName })
.whereNot({ msp, client })
.first();
if (oppositePortalRole) {
// Role exists in opposite portal, create new one for this portal
const [role] = await knex('roles')
.insert({
tenant,
role_name: roleName,
description,
msp,
client,
role_id: knex.raw('gen_random_uuid()'),
created_at: knex.fn.now()
})
.returning('*');
return role;
} else {
// No role with this name exists, create it
const [role] = await knex('roles')
.insert({
tenant,
role_name: roleName,
description,
msp,
client,
role_id: knex.raw('gen_random_uuid()'),
created_at: knex.fn.now()
})
.returning('*');
return role;
}
}
// Update existing role's description if needed
if (existing.description !== description) {
await knex('roles')
.where({ tenant, role_id: existing.role_id })
.update({ description });
}
return existing;
};
// Helper function to assign permission to role
const assignPermissionToRole = async (tenant, roleId, permissionId) => {
const existing = await knex('role_permissions')
.where({ tenant, role_id: roleId, permission_id: permissionId })
.first();
if (!existing) {
await knex('role_permissions').insert({
tenant,
role_id: roleId,
permission_id: permissionId,
created_at: knex.fn.now()
});
}
};
// Get all tenants
const tenants = await knex('tenants').select('tenant');
for (const { tenant } of tenants) {
console.log(`Processing tenant: ${tenant}`);
// First, handle Admin role conversions
// Convert all existing "Admin" roles to MSP Admin (they were MSP admins before)
// This needs to happen BEFORE any other role conversions to ensure proper assignment
const adminRolesWithoutFlags = await knex('roles')
.where({ tenant, role_name: 'Admin' })
.where(function() {
this.whereNull('msp').orWhereNull('client');
});
if (adminRolesWithoutFlags.length > 0) {
console.log(`Converting ${adminRolesWithoutFlags.length} existing Admin roles to MSP Admin for tenant ${tenant}`);
await knex('roles')
.where({ tenant, role_name: 'Admin' })
.where(function() {
this.whereNull('msp').orWhereNull('client');
})
.update({
msp: true,
client: false,
description: 'Full system administrator access'
});
}
// First, clean up any duplicate roles that might exist from previous migrations
// Find all roles grouped by name and portal flags
const duplicateRoles = await knex('roles')
.select('role_name', 'msp', 'client')
.count('* as count')
.where({ tenant })
.groupBy('role_name', 'msp', 'client')
.having(knex.raw('count(*) > 1'));
for (const dup of duplicateRoles) {
console.log(`Found ${dup.count} duplicate "${dup.role_name}" roles in ${dup.msp ? 'MSP' : 'Client'} portal`);
// Get all roles with this name and portal flags
const roles = await knex('roles')
.where({
tenant,
role_name: dup.role_name,
msp: dup.msp,
client: dup.client
})
.orderBy('created_at', 'asc');
if (roles.length > 1) {
// Keep the first (oldest) role
const keepRole = roles[0];
const deleteRoleIds = roles.slice(1).map(r => r.role_id);
// Move any user assignments from duplicate roles to the kept role
for (const deleteRoleId of deleteRoleIds) {
// Get users assigned to the duplicate role
const userAssignments = await knex('user_roles')
.where({ tenant, role_id: deleteRoleId });
for (const assignment of userAssignments) {
// Check if user already has the kept role
const existingAssignment = await knex('user_roles')
.where({
tenant,
user_id: assignment.user_id,
role_id: keepRole.role_id
})
.first();
if (!existingAssignment) {
// Move assignment to kept role
await knex('user_roles')
.where({
tenant,
user_id: assignment.user_id,
role_id: deleteRoleId
})
.update({ role_id: keepRole.role_id });
} else {
// User already has kept role, delete duplicate assignment
await knex('user_roles')
.where({
tenant,
user_id: assignment.user_id,
role_id: deleteRoleId
})
.delete();
}
}
// Delete role_permissions for duplicate role
await knex('role_permissions')
.where({ tenant, role_id: deleteRoleId })
.delete();
// Delete the duplicate role
await knex('roles')
.where({ tenant, role_id: deleteRoleId })
.delete();
console.log(`Deleted duplicate role ${deleteRoleId} and moved assignments to ${keepRole.role_id}`);
}
}
}
// Now convert existing Client and Client_Admin roles from MSP to Client portal
// This preserves user assignments while moving roles to correct portal
// Convert 'Client' role (case-insensitive) to 'User' role in client portal
const existingClientRoles = await knex('roles')
.where({ tenant })
.where(knex.raw('LOWER(role_name) = LOWER(?)', ['Client']))
.select('*');
if (existingClientRoles.length > 0) {
console.log(`Converting ${existingClientRoles.length} Client role(s) to User role for tenant ${tenant}`);
for (const role of existingClientRoles) {
await knex('roles')
.where({ tenant, role_id: role.role_id })
.update({
role_name: 'User',
description: 'Standard client portal user',
msp: false,
client: true
});
}
}
// Convert 'Client_Admin' role (case-insensitive) to 'Admin' role in client portal
const existingClientAdminRoles = await knex('roles')
.where({ tenant })
.where(knex.raw('LOWER(role_name) = LOWER(?)', ['Client_Admin']))
.select('*');
if (existingClientAdminRoles.length > 0) {
console.log(`Converting ${existingClientAdminRoles.length} Client_Admin role(s) to Admin role in client portal for tenant ${tenant}`);
for (const role of existingClientAdminRoles) {
await knex('roles')
.where({ tenant, role_id: role.role_id })
.update({
role_name: 'Admin',
description: 'Client portal administrator',
msp: false,
client: true
});
}
}
// Also handle any other MSP roles that don't have flags set yet
const mspRoleNames = ['Manager', 'Technician', 'Finance', 'Project Manager', 'Dispatcher'];
for (const roleName of mspRoleNames) {
await knex('roles')
.where({ tenant, role_name: roleName })
.whereNull('msp') // Only update roles that haven't been flagged yet
.update({
msp: true,
client: false
});
}
// Clean up any remaining Client_Admin roles after conversion (case-insensitive)
// Note: We already converted Client_Admin to Admin in client portal above,
// so this is just a safety check to ensure no orphaned Client_Admin roles remain
const remainingClientAdminRoles = await knex('roles')
.where({ tenant })
.where(knex.raw('LOWER(role_name) = LOWER(?)', ['Client_Admin']))
.count('* as count');
if (remainingClientAdminRoles[0].count > 0) {
console.log(`WARNING: Found ${remainingClientAdminRoles[0].count} remaining Client_Admin roles for tenant ${tenant} after conversion. This should not happen.`);
// Don't delete - log for investigation
}
// Create MSP Roles
const mspAdminRole = await ensureRole(tenant, 'Admin', 'Full system administrator access', true, false);
const mspFinanceRole = await ensureRole(tenant, 'Finance', 'Financial operations and billing management', true, false);
const mspTechnicianRole = await ensureRole(tenant, 'Technician', 'Technical support and ticket management', true, false);
const mspProjectManagerRole = await ensureRole(tenant, 'Project Manager', 'Project planning and management', true, false);
const mspDispatcherRole = await ensureRole(tenant, 'Dispatcher', 'Technician scheduling and dispatch', true, false);
// Create Client Roles
const clientUserRole = await ensureRole(tenant, 'User', 'Standard client portal user', false, true);
const clientFinanceRole = await ensureRole(tenant, 'Finance', 'Client financial operations', false, true);
// Only create client Admin role if there are no existing Admin roles without flags
// This prevents converting pre-existing MSP admins into client admins
let clientAdminRole;
const existingAdminWithoutFlags = await knex('roles')
.where({ tenant, role_name: 'Admin' })
.where(function() {
this.whereNull('msp').orWhereNull('client');
})
.first();
if (!existingAdminWithoutFlags) {
// Safe to create client admin role
clientAdminRole = await ensureRole(tenant, 'Admin', 'Client portal administrator', false, true);
} else {
// Use the existing client admin role if it exists
clientAdminRole = await knex('roles')
.where({ tenant, role_name: 'Admin', msp: false, client: true })
.first();
if (!clientAdminRole) {
// Create it if it doesn't exist yet
clientAdminRole = await ensureRole(tenant, 'Admin', 'Client portal administrator', false, true);
}
}
// Define all permissions needed
const permissions = [
// Asset permissions - MSP only
{ resource: 'asset', action: 'create', msp: true, client: false, description: 'Create new assets and equipment records' },
{ resource: 'asset', action: 'read', msp: true, client: false, description: 'View asset details and inventory' },
{ resource: 'asset', action: 'update', msp: true, client: false, description: 'Modify asset information and status' },
{ resource: 'asset', action: 'delete', msp: true, client: false, description: 'Remove assets from the system' },
// Billing permissions - MSP only
{ resource: 'billing', action: 'create', msp: true, client: false, description: 'Create billing records and charges' },
{ resource: 'billing', action: 'read', msp: true, client: false, description: 'View billing information and history' },
{ resource: 'billing', action: 'update', msp: true, client: false, description: 'Modify billing records and rates' },
{ resource: 'billing', action: 'delete', msp: true, client: false, description: 'Remove billing records' },
{ resource: 'billing', action: 'reconcile', msp: true, client: false, description: 'Reconcile billing discrepancies and adjustments' },
// Client permissions - MSP only
{ resource: 'client', action: 'create', msp: true, client: false, description: 'Add new client accounts' },
{ resource: 'client', action: 'read', msp: true, client: false, description: 'View client information and details' },
{ resource: 'client', action: 'update', msp: true, client: false, description: 'Modify client account information' },
{ resource: 'client', action: 'delete', msp: true, client: false, description: 'Remove client accounts' },
// Company permissions - Read and update for clients
{ resource: 'company', action: 'create', msp: true, client: false, description: 'Create new company profiles' },
{ resource: 'company', action: 'read', msp: true, client: true, description: 'View company information and settings' },
{ resource: 'company', action: 'update', msp: true, client: true, description: 'Edit company details and preferences' },
{ resource: 'company', action: 'delete', msp: true, client: false, description: 'Remove company profiles' },
// Contact permissions - MSP only
{ resource: 'contact', action: 'create', msp: true, client: false, description: 'Add new contacts to companies' },
{ resource: 'contact', action: 'read', msp: true, client: false, description: 'View contact information' },
{ resource: 'contact', action: 'update', msp: true, client: false, description: 'Edit contact details' },
{ resource: 'contact', action: 'delete', msp: true, client: false, description: 'Remove contacts from the system' },
// Credit permissions - MSP only
{ resource: 'credit', action: 'create', msp: true, client: false, description: 'Issue credits to accounts' },
{ resource: 'credit', action: 'read', msp: true, client: false, description: 'View credit balances and history' },
{ resource: 'credit', action: 'update', msp: true, client: false, description: 'Modify credit amounts and details' },
{ resource: 'credit', action: 'delete', msp: true, client: false, description: 'Remove credit records' },
{ resource: 'credit', action: 'transfer', msp: true, client: false, description: 'Transfer credits between accounts' },
{ resource: 'credit', action: 'apply', msp: true, client: false, description: 'Apply credits to invoices or charges' },
// Document permissions - Available to clients
{ resource: 'document', action: 'create', msp: true, client: true, description: 'Upload and create documents' },
{ resource: 'document', action: 'read', msp: true, client: true, description: 'View and download documents' },
{ resource: 'document', action: 'update', msp: true, client: true, description: 'Edit document metadata and content' },
{ resource: 'document', action: 'delete', msp: true, client: false, description: 'Delete documents from the system' },
// Invoice permissions - MSP only
{ resource: 'invoice', action: 'create', msp: true, client: false, description: 'Create new invoices' },
{ resource: 'invoice', action: 'read', msp: true, client: false, description: 'View invoice details and history' },
{ resource: 'invoice', action: 'update', msp: true, client: false, description: 'Modify invoice line items and details' },
{ resource: 'invoice', action: 'delete', msp: true, client: false, description: 'Delete draft invoices' },
{ resource: 'invoice', action: 'generate', msp: true, client: false, description: 'Generate invoices from billable items' },
{ resource: 'invoice', action: 'finalize', msp: true, client: false, description: 'Finalize and lock invoices' },
{ resource: 'invoice', action: 'send', msp: true, client: false, description: 'Send invoices to clients' },
// Profile permissions - MSP only
{ resource: 'profile', action: 'create', msp: true, client: false, description: 'Create new user profiles' },
{ resource: 'profile', action: 'read', msp: true, client: false, description: 'View user profile information' },
{ resource: 'profile', action: 'update', msp: true, client: false, description: 'Edit user profile details' },
{ resource: 'profile', action: 'delete', msp: true, client: false, description: 'Remove user profiles' },
// Project permissions - Read-only for clients
{ resource: 'project', action: 'create', msp: true, client: false, description: 'Create new projects' },
{ resource: 'project', action: 'read', msp: true, client: true, description: 'View project details and status' },
{ resource: 'project', action: 'update', msp: true, client: false, description: 'Modify project information and timeline' },
{ resource: 'project', action: 'delete', msp: true, client: false, description: 'Delete projects and associated data' },
// Tag permissions - MSP only
{ resource: 'tag', action: 'create', msp: true, client: false, description: 'Create new tags for categorization' },
{ resource: 'tag', action: 'read', msp: true, client: false, description: 'View available tags' },
{ resource: 'tag', action: 'update', msp: true, client: false, description: 'Edit tags content and colors' },
{ resource: 'tag', action: 'delete', msp: true, client: false, description: 'Remove tags from the system' },
// Technician dispatch permissions
{ resource: 'technician_dispatch', action: 'create', msp: true, client: false, description: 'Create dispatch schedules for technicians' },
{ resource: 'technician_dispatch', action: 'read', msp: true, client: false, description: 'View technician schedules and assignments' },
{ resource: 'technician_dispatch', action: 'update', msp: true, client: false, description: 'Modify dispatch assignments and timing' },
{ resource: 'technician_dispatch', action: 'delete', msp: true, client: false, description: 'Remove dispatch assignments' },
// Ticket permissions
{ resource: 'ticket', action: 'create', msp: true, client: true, description: 'Create support tickets' },
{ resource: 'ticket', action: 'read', msp: true, client: true, description: 'View ticket details and history' },
{ resource: 'ticket', action: 'update', msp: true, client: true, description: 'Update ticket status and add comments' },
{ resource: 'ticket', action: 'delete', msp: true, client: false, description: 'Delete tickets from the system' },
// Time entry permissions
{ resource: 'timeentry', action: 'create', msp: true, client: false, description: 'Log time entries for work performed' },
{ resource: 'timeentry', action: 'read', msp: true, client: false, description: 'View time entry records' },
{ resource: 'timeentry', action: 'update', msp: true, client: false, description: 'Edit time entry details and duration' },
{ resource: 'timeentry', action: 'delete', msp: true, client: false, description: 'Remove time entries' },
// Timesheet permissions
{ resource: 'timesheet', action: 'create', msp: true, client: false, description: 'Create timesheets for time tracking' },
{ resource: 'timesheet', action: 'read', msp: true, client: false, description: 'View timesheet summaries and details' },
{ resource: 'timesheet', action: 'update', msp: true, client: false, description: 'Modify timesheet entries' },
{ resource: 'timesheet', action: 'delete', msp: true, client: false, description: 'Delete timesheets' },
{ resource: 'timesheet', action: 'submit', msp: true, client: false, description: 'Submit timesheets for approval' },
{ resource: 'timesheet', action: 'approve', msp: true, client: false, description: 'Approve or reject submitted timesheets' },
// User permissions - Available in both portals as each manages their own users
{ resource: 'user', action: 'create', msp: true, client: true, description: 'Create new user accounts' },
{ resource: 'user', action: 'read', msp: true, client: true, description: 'View user information and status' },
{ resource: 'user', action: 'update', msp: true, client: true, description: 'Edit user details and permissions' },
{ resource: 'user', action: 'delete', msp: true, client: true, description: 'Remove user accounts' },
{ resource: 'user', action: 'invite', msp: true, client: true, description: 'Send invitations to new users' },
{ resource: 'user', action: 'reset_password', msp: true, client: true, description: 'Reset user passwords' },
// User schedule permissions
{ resource: 'user_schedule', action: 'create', msp: true, client: false, description: 'Create user work schedules' },
{ resource: 'user_schedule', action: 'read', msp: true, client: false, description: 'View user availability and schedules' },
{ resource: 'user_schedule', action: 'update', msp: true, client: false, description: 'Modify user schedule assignments' },
{ resource: 'user_schedule', action: 'delete', msp: true, client: false, description: 'Remove user schedules' },
// Settings permissions
{ resource: 'ticket_settings', action: 'read', msp: true, client: false, description: 'View ticket system configuration' },
{ resource: 'ticket_settings', action: 'update', msp: true, client: false, description: 'Configure ticket workflows and rules' },
{ resource: 'user_settings', action: 'read', msp: true, client: true, description: 'View personal user preferences' },
{ resource: 'user_settings', action: 'update', msp: true, client: true, description: 'Update personal preferences and notifications' },
{ resource: 'system_settings', action: 'read', msp: true, client: false, description: 'View system-wide configuration' },
{ resource: 'system_settings', action: 'update', msp: true, client: false, description: 'Modify system configuration and defaults' },
{ resource: 'security_settings', action: 'read', msp: true, client: false, description: 'View security policies and settings' },
{ resource: 'security_settings', action: 'update', msp: true, client: false, description: 'Configure security policies and access controls' },
{ resource: 'timeentry_settings', action: 'read', msp: true, client: false, description: 'View time tracking configuration' },
{ resource: 'timeentry_settings', action: 'update', msp: true, client: false, description: 'Configure time tracking rules and defaults' },
{ resource: 'billing_settings', action: 'create', msp: true, client: false, description: 'Create billing configuration profiles' },
{ resource: 'billing_settings', action: 'read', msp: true, client: false, description: 'View billing rates and rules' },
{ resource: 'billing_settings', action: 'update', msp: true, client: false, description: 'Modify billing rates and configuration' },
{ resource: 'billing_settings', action: 'delete', msp: true, client: false, description: 'Remove billing configuration profiles' },
];
// Create all permissions
const permissionMap = new Map();
for (const perm of permissions) {
const permission = await ensurePermission(tenant, perm.resource, perm.action, perm.msp, perm.client, perm.description);
permissionMap.set(`${perm.resource}:${perm.action}`, permission.permission_id);
}
// MSP Admin - Full access to all MSP permissions
for (const perm of permissions.filter(p => p.msp)) {
const permId = permissionMap.get(`${perm.resource}:${perm.action}`);
if (permId) {
await assignPermissionToRole(tenant, mspAdminRole.role_id, permId);
}
}
// MSP Finance - Specific permissions as defined
const financePermissions = [
'asset:read',
'billing:create', 'billing:read', 'billing:update', 'billing:delete', 'billing:reconcile',
'client:create', 'client:read', 'client:update', 'client:delete',
'contact:create', 'contact:read', 'contact:update', 'contact:delete',
'credit:create', 'credit:read', 'credit:update', 'credit:delete', 'credit:transfer', 'credit:apply',
'document:create', 'document:read', 'document:update', 'document:delete',
'invoice:create', 'invoice:read', 'invoice:update', 'invoice:delete', 'invoice:generate', 'invoice:finalize',
'profile:create', 'profile:read', 'profile:update', 'profile:delete',
'project:read', 'project:update',
'tag:create', 'tag:read',
'technician_dispatch:read',
'ticket:read', 'ticket:update',
'timeentry:create', 'timeentry:read', 'timeentry:update', 'timeentry:delete',
'timesheet:create', 'timesheet:read', 'timesheet:update', 'timesheet:delete', 'timesheet:submit',
'user:read',
'user_schedule:read',
'billing_settings:create', 'billing_settings:read', 'billing_settings:update', 'billing_settings:delete'
];
for (const permKey of financePermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, mspFinanceRole.role_id, permId);
}
}
// MSP Technician - Ticket and time management focused
const technicianPermissions = [
'asset:read',
'contact:read',
'document:create', 'document:read', 'document:update',
'profile:read', 'profile:update',
'project:read',
'tag:read',
'ticket:create', 'ticket:read', 'ticket:update',
'timeentry:create', 'timeentry:read', 'timeentry:update',
'timesheet:create', 'timesheet:read', 'timesheet:update', 'timesheet:submit',
'user:read',
'user_schedule:read',
'user_settings:read', 'user_settings:update'
];
for (const permKey of technicianPermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, mspTechnicianRole.role_id, permId);
}
}
// MSP Project Manager - Project and resource management
const projectManagerPermissions = [
'asset:read',
'client:read',
'contact:create', 'contact:read', 'contact:update',
'document:create', 'document:read', 'document:update', 'document:delete',
'invoice:read',
'profile:read',
'project:create', 'project:read', 'project:update', 'project:delete',
'tag:create', 'tag:read', 'tag:update',
'technician_dispatch:read',
'ticket:create', 'ticket:read', 'ticket:update',
'timeentry:read',
'timesheet:read', 'timesheet:approve',
'user:read',
'user_schedule:read'
];
for (const permKey of projectManagerPermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, mspProjectManagerRole.role_id, permId);
}
}
// MSP Dispatcher - Scheduling and dispatch
const dispatcherPermissions = [
'contact:read',
'profile:read',
'technician_dispatch:create', 'technician_dispatch:read', 'technician_dispatch:update', 'technician_dispatch:delete',
'ticket:read', 'ticket:update',
'user:read',
'user_schedule:create', 'user_schedule:read', 'user_schedule:update', 'user_schedule:delete'
];
for (const permKey of dispatcherPermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, mspDispatcherRole.role_id, permId);
}
}
// Client Admin - Full access to client permissions
if (clientAdminRole) {
for (const perm of permissions.filter(p => p.client)) {
const permId = permissionMap.get(`${perm.resource}:${perm.action}`);
if (permId) {
await assignPermissionToRole(tenant, clientAdminRole.role_id, permId);
}
}
}
// Client Finance - Financial visibility
const clientFinancePermissions = [
'billing:read',
'client:read',
'contact:read',
'credit:read',
'document:read',
'invoice:read',
'profile:read',
'user_settings:read', 'user_settings:update'
];
for (const permKey of clientFinancePermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, clientFinanceRole.role_id, permId);
}
}
// Client User - Basic access
const clientUserPermissions = [
'asset:read',
'contact:create', 'contact:read', 'contact:update',
'document:create', 'document:read',
'profile:read', 'profile:update',
'project:read',
'tag:read',
'ticket:create', 'ticket:read', 'ticket:update',
'user_settings:read', 'user_settings:update',
'user:read' // Can see other users but not manage them
];
for (const permKey of clientUserPermissions) {
const permId = permissionMap.get(permKey);
if (permId) {
await assignPermissionToRole(tenant, clientUserRole.role_id, permId);
}
}
}
// Clean up deprecated permissions
console.log('Removing deprecated permissions...');
// Resources to completely remove
const deprecatedResources = ['category', 'comment', 'client_password', 'client_profile', 'company_setting'];
for (const resource of deprecatedResources) {
// First remove any role_permissions assignments
await knex('role_permissions')
.whereIn('permission_id', function() {
this.select('permission_id')
.from('permissions')
.where('resource', resource);
})
.delete();
// Then remove the permissions themselves
const deletedCount = await knex('permissions')
.where('resource', resource)
.delete();
if (deletedCount > 0) {
console.log(`Removed ${deletedCount} deprecated ${resource} permissions`);
}
}
// Update existing permissions to restrict client portal access
console.log('Updating client portal permissions to match security requirements...');
// Resources that should NOT be available to client portal
const mspOnlyResources = [
'asset', 'billing', 'client', 'contact', 'credit', 'invoice',
'profile', 'tag', 'priority', 'notification'
];
// Update these resources to be MSP-only
for (const resource of mspOnlyResources) {
const updated = await knex('permissions')
.where('resource', resource)
.where('client', true)
.update({ client: false });
if (updated > 0) {
console.log(`Updated ${updated} ${resource} permissions to be MSP-only`);
}
}
// Ensure the 5 allowed resources have correct permissions for client portal
// These are: user, ticket, project (read-only), company (read/update), document
const clientAllowedUpdates = [
{ resource: 'project', actions: ['read'] },
{ resource: 'company', actions: ['read', 'update'] }
];
for (const { resource, actions } of clientAllowedUpdates) {
const updated = await knex('permissions')
.where('resource', resource)
.whereIn('action', actions)
.where('client', false)
.update({ client: true });
if (updated > 0) {
console.log(`Updated ${updated} ${resource} permissions to be available in client portal`);
}
}
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function() {
// This migration is designed to be idempotent and only updates existing data
// Rolling back would require removing the msp/client columns which is handled by the schema migration
// No specific rollback needed for the data updates
};