PSA/shared/models/userModel.ts
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

322 lines
8.2 KiB
TypeScript

/**
* Shared User Model - Core business logic for user operations
* This model contains the essential user business logic extracted from
* server actions and used by both server actions and workflow actions.
*/
import { Knex } from 'knex';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
import { hashPassword } from '../utils/encryption';
import type {
IUser,
IRole,
CreatePortalUserInput,
CreatePortalUserResult,
PortalRoleOptions
} from '../interfaces/user.interfaces';
// Re-export types for convenience
export type {
CreatePortalUserInput,
CreatePortalUserResult,
PortalRoleOptions
};
// =============================================================================
// VALIDATION SCHEMAS
// =============================================================================
/**
* Portal user input validation schema
*/
export const portalUserInputSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
contactId: z.string().uuid('Contact ID must be a valid UUID'),
clientId: z.string().uuid('Client ID must be a valid UUID'),
tenantId: z.string().uuid('Tenant ID must be a valid UUID'),
firstName: z.string().optional(),
lastName: z.string().optional(),
roleId: z.string().uuid().optional(),
isClientAdmin: z.boolean().optional()
});
// =============================================================================
// PORTAL USER OPERATIONS
// =============================================================================
/**
* Determine the appropriate portal role for a user
* @param trx - Knex transaction
* @param options - Options for determining role
* @returns The role to assign or null if not found
*/
export async function determinePortalUserRole(
trx: Knex.Transaction,
options: PortalRoleOptions
): Promise<IRole | null> {
const { isClientAdmin, tenantId, roleId } = options;
// If a specific roleId is provided, validate and use it
if (roleId) {
const role = await trx('roles')
.where({
role_id: roleId,
tenant: tenantId,
client: true
})
.first();
if (!role) {
throw new Error('Invalid role ID or role is not a client portal role');
}
return role;
}
// Determine role based on isClientAdmin flag
const roleName = isClientAdmin ? 'admin' : 'user';
// Get the appropriate client portal role
let clientRole = await trx('roles')
.where({
tenant: tenantId,
client: true,
msp: false
})
.whereRaw('LOWER(role_name) = ?', [roleName])
.first();
if (!clientRole) {
throw new Error(`Client portal ${roleName} role not found for tenant`);
}
return clientRole;
}
/**
* Get the password field name for the users table.
*/
export async function getPasswordFieldName(_knex: Knex): Promise<string> {
return 'hashed_password';
}
/**
* Create a portal user in the database
* This is the core logic for creating client/portal users
*/
async function _createPortalUserInDBWithTrx(
trx: Knex.Transaction,
input: CreatePortalUserInput
): Promise<CreatePortalUserResult> {
try {
// Check if a client-portal user already exists for this email
// Allow the same email to exist for other user types (e.g., internal MSP users)
const existingUser = await trx('users')
.where({
email: input.email.toLowerCase(),
tenant: input.tenantId,
user_type: 'client'
})
.first();
if (existingUser) {
throw new Error('A client portal user with this email already exists');
}
// Get the contact to check is_client_admin flag if not explicitly provided
let isClientAdmin = input.isClientAdmin;
if (isClientAdmin === undefined) {
const contact = await trx('contacts')
.where({
contact_name_id: input.contactId,
tenant: input.tenantId
})
.first();
if (!contact) {
throw new Error('Contact not found');
}
isClientAdmin = contact.is_client_admin || false;
}
// Determine the role to assign
const roleToAssign = await determinePortalUserRole(trx, {
isClientAdmin: isClientAdmin || false,
tenantId: input.tenantId,
roleId: input.roleId
});
if (!roleToAssign) {
throw new Error('Unable to determine appropriate portal role');
}
// Hash the password
const hashedPassword = await hashPassword(input.password);
// Create the user with dynamic password field
const userData: any = {
user_id: uuidv4(),
tenant: input.tenantId,
email: input.email.toLowerCase(),
username: input.email.toLowerCase(),
contact_id: input.contactId,
user_type: 'client',
is_inactive: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
hashed_password: hashedPassword
};
// Add optional fields
if (input.firstName) userData.first_name = input.firstName;
if (input.lastName) userData.last_name = input.lastName;
// Insert the user
const [user] = await trx('users')
.insert(userData)
.returning('*');
// Assign the role
await trx('user_roles')
.insert({
user_id: user.user_id,
role_id: roleToAssign.role_id,
tenant: input.tenantId
});
return {
success: true,
userId: user.user_id,
roleId: roleToAssign.role_id
};
} catch (error) {
console.error('Error creating portal user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Create a portal user using an existing transaction
*/
export async function createPortalUserInDBWithTrx(
trx: Knex.Transaction,
input: CreatePortalUserInput
): Promise<CreatePortalUserResult> {
return _createPortalUserInDBWithTrx(trx, input);
}
/**
* Create a portal user, managing its own transaction
*/
export async function createPortalUserInDB(
knex: Knex,
input: CreatePortalUserInput
): Promise<CreatePortalUserResult> {
try {
const result = await knex.transaction(async (trx: Knex.Transaction) => {
return _createPortalUserInDBWithTrx(trx, input);
});
return result;
} catch (error) {
console.error('Error creating portal user:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Get portal users for a client
*/
export async function getPortalUsersForClient(
knex: Knex,
clientId: string,
tenantId: string
): Promise<IUser[]> {
try {
// Get all contacts for the client
const contacts = await knex('contacts')
.where({
client_id: clientId,
tenant: tenantId
})
.select('contact_name_id');
const contactIds = contacts.map(c => c.contact_name_id);
if (contactIds.length === 0) {
return [];
}
// Get all users associated with these contacts
const users = await knex('users')
.whereIn('contact_id', contactIds)
.andWhere({
tenant: tenantId,
user_type: 'client'
})
.select('*');
return users;
} catch (error) {
console.error('Error fetching portal users for client:', error);
return [];
}
}
/**
* Get available client portal roles
*/
export async function getClientPortalRoles(
knex: Knex,
tenantId: string
): Promise<IRole[]> {
try {
const roles = await knex('roles')
.where({
tenant: tenantId,
client: true,
msp: false
})
.select('*');
return roles;
} catch (error) {
console.error('Error fetching client portal roles:', error);
return [];
}
}
/**
* Validate portal user input
*/
export function validatePortalUserInput(input: unknown): {
valid: boolean;
data?: CreatePortalUserInput;
errors?: z.ZodError;
} {
try {
const validated = portalUserInputSchema.parse(input) as CreatePortalUserInput;
return {
valid: true,
data: validated
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
valid: false,
errors: error
};
}
throw error;
}
}