User Management
๐ Overviewโ
internal/api/v1/users/services/users.js handles comprehensive user management across the DashClicks platform, including user CRUD operations, scope/permission management, availability scheduling, phone/email management, work
load reassignment, device push tokens, and SMS recovery. Supports both main account and client dashboard contexts.
File Path: internal/api/v1/users/services/users.js
Lines of Code: 1,365
๐๏ธ Collections Usedโ
๐ Full Schema: See Database Collections Documentation
usersโ
- Operations: Full CRUD with complex permission logic
- Model:
shared/models/user.js - Usage Context: User profiles, authentication, scope management, availability scheduling
accountsโ
- Operations: Read for validation, lookups, parent-child relationships
- Model:
shared/models/account.js - Usage Context: Account ownership, business information, branding
support.conversationsโ
- Operations: Create/update for team member support conversations
- Model:
shared/models/conversation.js - Usage Context: Internal team chat for users
api_sessionsโ
- Operations: Delete on scope/permission changes
- Model:
shared/models/api-session.js - Usage Context: Force re-authentication after permission updates
deals, contacts, forms, templates, campaign_data, instasites, instareportsโ
- Operations: Count for associated data, reassignment on deletion
- Usage Context: Workload calculation and reassignment
support.rooms, support.inboxesโ
- Operations: Aggregation for conversation scope management
- Model:
shared/models/support-room.js,shared/models/support-inbox.js - Usage Context: Assign users to support conversations based on scope
expo_push_tokensโ
- Operations: Create/update/delete for mobile push notifications
- Model:
shared/models/expo-push-tokens.js - Usage Context: Mobile app push notification delivery
๐ Data Flowโ
sequenceDiagram
participant Admin
participant Controller
participant Service
participant Database
participant Email
participant Socket
Admin->>Controller: POST /users (create user)
Controller->>Service: createUser()
Service->>Database: Check duplicate email
Service->>Database: Create user + conversation
Service->>Database: Assign to support rooms (if scope)
Service->>Email: Send invitation email
Service->>Socket: TODO: Emit user_created
Service-->>Controller: Created user
Controller-->>Admin: Success response
Admin->>Controller: PATCH /users/:id (update scope)
Controller->>Service: updateUser() / updateScope()
Service->>Database: Update user
Service->>Socket: Emit supportUserDelete (if removed)
Service->>Database: Delete all sessions (force re-auth)
Service-->>Controller: Updated user
๐ง Business Logic & Functionsโ
User Retrieval & Searchโ
getUsers({ limit, page, account_id, account, filters, pending, archived, user, platform })โ
Purpose: Retrieves paginated list of users for an account with filtering by verification status, active state, and custom filters. Supports both regular and client dashboard contexts.
Parameters:
limit(Number) - Results per pagepage(Number, optional) - Page number (0-indexed if absent)account_id(ObjectId) - Account identifieraccount(ObjectId, optional) - Override account (for dashclicks general users)filters(Object, optional) - Additional MongoDB query filterspending(String) - 'true' to show unverified usersarchived(String) - 'true' to show inactive usersuser(Object) - Requesting user objectplatform(String) - 'client_dashboard' for client platform
Returns: Promise<{ users: Array, pagination: Object }> - Paginated user list
Business Logic Flow:
-
Build Base Query
- Default:
active: true,verified: { $ne: null } - Software filter: Exclude non-software users
- Override account if user has
dashclicks.generalpermission
- Default:
-
Handle Platform Context
- Client dashboard: Remove software filter, override account
- Regular platform: Apply verification filter
-
Apply Status Filters
pending == 'true': Show unverified users (verified: null)archived == 'true': Show inactive users (active: false)
-
Merge Custom Filters
- Apply additional filters from request
-
Parallel Queries
- Count total matching documents
- Fetch page of users with business population
-
Generate Pagination
- Calculate total pages, prev/next links
Key Business Rules:
- Software filter excludes system/bot accounts
- DashClicks general users can view across accounts
- Client dashboard has relaxed verification rules
- Pagination calculated with
generatePagination()utility
Example Usage:
const { users, pagination } = await userService.getUsers({
limit: 25,
page: 1,
account_id: req.account_id,
pending: 'false',
archived: 'false',
platform: 'client_dashboard',
});
search({ page, limit, pending, archived, account_id, search })โ
Purpose: Searches users by name, email, first name, or last name with pagination.
Parameters:
search(String) - Search query (case-insensitive regex)- Other params same as
getUsers()
Returns: Promise<{ users: Array, pagination: Object }>
Business Logic Flow:
-
Build Base Query (same as getUsers)
-
Add Search Conditions
$orquery across:name,email,first_name,last_name- Case-insensitive regex match
-
Execute Parallel Queries
Key Business Rules:
- Searches across 4 fields simultaneously
- Respects active/archived/pending filters
User Creation & Invitationโ
createUser({ newUser, account_id, query, user, account, req })โ
Purpose: Creates a new user with invitation email, conversation setup, and support room assignment. Supports client dashboard user creation.
Parameters:
newUser(Object) - User data:first_name,last_name(String, required)email(String, required)scope(Array, optional) - Permission scopessub_account_id(ObjectId, optional) - For client dashboard
account_id(ObjectId) - Target accountquery(Object):invitation_only('yes'/'no') - Update existing unverified usersend_invite('yes'/'no') - Send invitation emailplatform('client_dashboard' or undefined)
user,account,req- Context objects
Returns: Promise<{ user: Object, message: String }> - Created user + status message
Business Logic Flow:
-
Client Dashboard Context (if
platform == 'client_dashboard')- Lookup sub-account by
sub_account_id - Switch account/user context to sub-account
- Set
metadata.software: false(no regular access)
- Lookup sub-account by
-
Duplicate Check
- Find existing user by email + account
- If found and not invitation_only: throw
badRequest
-
Prepare User Data
- Combine first_name + last_name โ
name - Set
verified: null,active: true - Generate JWT reset token (expires 84600s = 23.5 hours)
- Set metadata:
software: true,service: true
- Combine first_name + last_name โ
-
Scope Validation
- If non-owner creating user: check permission
- Admin can only assign scopes they have
- Cannot assign 'system' scope
-
Add Default Scopes
- Force include: 'users.me', 'files'
- Use Set to deduplicate
-
Set Default Availability
- Monday-Friday: 9:00 AM - 5:30 PM
-
Transaction: Create/Update User
- If invitation_only + existing: Update existing user
- Else: Create new user
- Create default resources asynchronously (
createDefault())
-
Create/Update Conversation
- Upsert support conversation for user
- Set status: 'active',
support_live_seat: false
-
Assign to Support Rooms (if conversation scope)
- Aggregate all support rooms for account
- Extract contact IDs
- Add user as follower to all contacts
-
Send Invitation Email (if send_invite == 'yes')
- Use SendGrid template
d-068220b420984ac1b734b083e248f370 - Include reset token link
- Use business branding and contact info
- Set message status: 'SUCCESS' or 'FAILED_TO_SEND_INVITATION_EMAIL'
- Use SendGrid template
Key Business Rules:
- Email must be unique per account
- invitation_only allows updating unverified users
- Client dashboard users cannot access regular software
- Reset token valid for ~23.5 hours
- Conversation scope adds user to all support conversations
- Default availability is weekdays 9-5:30
Error Handling:
badRequest(400): Duplicate emailforbidden(403): Insufficient permissions to assign scope
Side Effects:
- โ ๏ธ Creates user record
- โ ๏ธ Creates conversation record
- โ ๏ธ Asynchronously creates default resources
- โ ๏ธ Sends invitation email
- โ ๏ธ Adds user as follower to support contacts
- โ ๏ธ TODO: Should emit socket notification (commented)
User Updates & Profileโ
updateUser({ id, userData, account_id, uid, auth_user })โ
Purpose: Updates user profile, permissions, availability, and metadata. Validates unavailability against availability windows.
Parameters:
id(ObjectId) - User to updateuserData(Object) - Fields to update:first_name,last_name,email,phone, etc.password(String) - Will be hashedscope(Array) - Permission changesavailability(Array) - Weekly scheduleunavailability(Array) - Time-off blocks
account_id(ObjectId) - Account contextuid(ObjectId) - Requesting user IDauth_user(Object) - Requesting user object
Returns: Promise<Object> - Updated user document
Business Logic Flow:
-
Find Target User
- Query by id + account_id
- Throw
notFoundif not exists
-
Permission Check
- If not self-update: require owner or admin role
- If self-update: cannot change own scope
-
Email Uniqueness Check
- If changing email: verify no duplicate
-
Update Full Name
- If first_name or last_name changed: regenerate
namefield
- If first_name or last_name changed: regenerate
-
Password Hashing
- If password provided: generate salt + hash
-
Scope Validation (if scope changed)
- Non-owners: check admin role
- Verify auth_user has all scopes being assigned
- Cannot assign 'system' scope
- conversation.manager requires admin role
-
Unavailability Validation (if unavailability provided)
- For each unavailability entry:
- If not all_day: validate time slots
- Parse date in user's timezone
- Find day's availability window
- Convert times to minutes (12-hour โ 24-hour)
- Verify unavailability within availability window
- Verify start_time < end_time
- For each unavailability entry:
-
Transaction: Update User + Related Data
- Reactivate Conversation (if archived due to user_archived)
- Change status from 'archived' โ 'active'
- Remove Support Access (if removing conversation scope)
- Emit socket event: 'supportUserDelete'
- Update User Document
- Emit Socket (if availability/unavailability changed)
- Event: 'user_updated' to all account users
- Assign to Support Rooms (if adding conversation scope)
- Aggregate rooms by scope type:
conversationorconversation.all: All account roomsconversation.assigned_inbox: Only assigned inbox rooms
- Extract contacts, add user as follower
- Aggregate rooms by scope type:
- Reactivate Conversation (if archived due to user_archived)
-
Force Re-Authentication (if scope changed)
- Delete all user sessions
- User must log in again with new permissions
Key Business Rules:
- Cannot change own scope (prevents privilege escalation)
- Email must remain unique per account
- Unavailability must fall within availability windows
- Time validation uses moment-timezone for accuracy
- Scope changes force session logout
- conversation.manager restricted to admin users
- Socket emits for real-time UI updates
Error Handling:
notFound(404): User doesn't existforbidden(403): Insufficient permissionsbadRequest(400): Duplicate email, invalid unavailability times
Unavailability Time Validation Algorithm:
const timeToMinutes = timeStr => {
const [time, period] = timeStr.split(' ');
let [hours, minutes] = time.split(':').map(Number);
if (period === 'PM' && hours !== 12) hours += 12;
if (period === 'AM' && hours === 12) hours = 0;
return hours * 60 + minutes;
};
Side Effects:
- โ ๏ธ Updates user document
- โ ๏ธ May reactivate conversation
- โ ๏ธ May emit socket events
- โ ๏ธ May add user to support conversations
- โ ๏ธ Deletes all user sessions (if scope changed)
updateScope({ id, account_id, uid, auth_user, newScopes, removeScopes })โ
Purpose: Specialized function to add or remove permission scopes without full user update. Handles conversation access changes.
Parameters:
newScopes(Array) - Scopes to addremoveScopes(Array) - Scopes to remove- Other params same as updateUser
Returns: Promise<Object> - Updated user
Business Logic Flow:
-
Permission Validation (same as updateUser)
-
Scope Set Operations
- Convert current scope to Set
- Add all newScopes
- Remove all removeScopes
- Deduplicate
-
Transaction: Update Scope + Related
- Reactivate conversation if archived
- Remove support access if removing conversation.support
- Update user.scope
- Assign to support rooms if adding conversation scope
-
Force Re-Authentication
- Delete all sessions
Key Difference from updateUser:
- Focused only on scope changes
- Additive and subtractive operations
- No validation of other user fields
User Deletion & Reassignmentโ
deleteUser({ id, account_id, uid, auth_user, hard_delete, assigned_to })โ
Purpose: Soft or hard deletes a user with optional workload reassignment. Archives conversations and forces session logout.
Parameters:
id(ObjectId) - User to deletehard_delete(Boolean) - True = permanent delete, False = set active: falseassigned_to(ObjectId, optional) - User to reassign workload to- Other params standard
Returns: Promise<Object|undefined> - Updated user (soft delete) or undefined (hard delete)
Business Logic Flow:
-
Find User + Permission Check
- Cannot delete self
- Cannot delete owner
- Requires admin or owner permission
-
Workload Reassignment (if assigned_to provided)
- Call
deleteUser()utility with assigned_to parameter - Reassigns deals, contacts, forms, templates, etc.
- Throw
badRequestif reassignment fails
- Call
-
Hard Delete Path
- Delete user document from database
- Archive conversation with status: 'deleted'
- Set metadata:
trigger_type: 'user_deleted' - Emit socket event: 'supportUserDelete'
-
Soft Delete Path (default)
- Set
user.active = false - Check if user has reassignable project role
- If role is FULFILMENT_SPECIALIST or ACCOUNT_MANAGER:
- Call
reassignUserWorkloadUtil()to reassign projects - Log errors but don't fail deletion
- Call
- Archive conversation with status: 'archived'
- Set metadata:
trigger_type: 'user_archived' - Emit socket event: 'supportUserDelete'
- Set
-
Force Logout
- Delete all user sessions
Key Business Rules:
- Soft delete is default (data retention)
- Hard delete is permanent
- Owner users cannot be deleted
- Cannot delete yourself
- Workload reassignment is optional but recommended
- Project roles trigger automatic workload reassignment
- Reassignment failures logged but don't block deletion
- Conversations archived (not deleted) for audit
Error Handling:
notFound(404): User doesn't existforbidden(403): Insufficient permissions, is owner, or self-deletebadRequest(400): Reassignment failure
Side Effects:
- โ ๏ธ Deletes or deactivates user
- โ ๏ธ Archives conversation
- โ ๏ธ Emits socket event
- โ ๏ธ Deletes all sessions
- โ ๏ธ May reassign deals, contacts, projects, etc.
Phone & Email Managementโ
addPhoneToUser({ id, account_id, auth_user, uid, userData })โ
Purpose: Adds an additional phone number to a user's profile.
Parameters:
userData(Object):phone(String, required) - Phone numbername(String, optional, default: 'default') - Phone label
Returns: Promise<Object> - Updated user
Business Logic Flow:
-
Permission Check
- Cannot modify owner (unless you are owner)
- Admin can modify any non-owner
- User can modify self
-
Add Phone
- Use
$addToSetto append toadditional_info.phonesarray - Set
main: false(not primary phone)
- Use
Error Handling:
- DuplicateKey (11000): Phone already belongs to another user
updatePhone({ id, phoneid, account_id, auth_user, uid, userData })โ
Purpose: Updates an existing additional phone number.
Parameters:
phoneid(ObjectId) - Phone entry ID in arrayuserData(Object):phone(String, optional) - New numbername(String, optional) - New label
Returns: Promise<Object> - Updated user
Business Logic Flow:
-
Permission Check (same as addPhone)
-
Update Phone Entry
- Use
$setwith array positional operator ($) - Only update provided fields
- Use
updateEmail({ email, account, authToken, auth_user })โ
Purpose: Adds a SendGrid-verified email to user's sendgrid_emails array for sending campaigns.
Parameters:
email(String) - Email to addaccount(Object) - Account document with SendGrid domainsauthToken(String) - For external API calls
Returns: Promise<Array> - Updated sendgrid_emails array
Business Logic Flow:
-
Validate Email Format
- Regex validation, throw
badRequestif invalid
- Regex validation, throw
-
Extract Domain + Find SendGrid Domain
- Split email at @ to get domain
- Find matching domain in
account.sendgrid.domains - Throw
notFoundif no match
-
Validate Domain via External API
POST /v1/e/sendgrid/subusers/domains/${id}/validate- Check if domain is valid
-
Add Email to User
- Check if already in
sendgrid_emailsarray - Use
$pushto add if not duplicate
- Check if already in
Key Business Rules:
- Email domain must be registered in SendGrid
- Domain must be validated before adding email
- Prevents duplicate emails in array
Error Handling:
badRequest(400): Invalid email format, domain not validated, duplicate emailnotFound(404): Domain not in SendGrid configuration
deleteEmail({ email, auth_user })โ
Purpose: Removes an email from user's sendgrid_emails array.
Parameters:
email(String) - Email to remove
Returns: Promise<Array> - Updated sendgrid_emails array
Business Logic Flow:
- Lowercase Email
- Remove from Array
- Use
$pulloperator
- Use
Associated Data & Recoveryโ
associated({ id, account_id })โ
Purpose: Counts how many entities are owned/created by a user across the platform. Used for reassignment planning.
Parameters:
id(ObjectId) - User ID
Returns: Promise<Object> - Count object with keys:
deals,contacts,forms,template,inboundRoundRobin,inbound,instasites,instareports
Business Logic Flow:
-
Parallel Count Queries (Promise.all):
- Deals:
{ owner: userId } - Contacts:
{ owner: userId } - Forms:
{ owner: userId } - Templates:
{ user: userId } - Inbound Round Robin:
{ selected_user.id: userId, $expr: { $gte: [{ $size: '$selected_user' }, 2] } } - Inbound:
{ owner: userId, $expr: { $lte: [{ $size: '$selected_user' }, 1] } } - Instasites:
{ created_by: userId } - Instareports:
{ created_by: userId }
- Deals:
-
Return Aggregated Counts
Use Case: Before deleting user, show admin how much data will be affected
recovery({ account_id, phone_number, code })โ
Purpose: SMS-based account recovery. Sends recovery code or validates code to retrieve email.
Parameters: Step 1 (send code):
account_id(ObjectId)phone_number(String)
Step 2 (validate code):
account_id(ObjectId)code(String) - 5-digit code
Returns:
- Step 1:
{ message: 'text message sent' } - Step 2:
{ message: 'SUCCESS', data: { email } }
Business Logic Flow:
Step 1: Send Recovery Code
- Find user by phone (primary or additional_info.phones)
- If found (in transaction):
- Generate 5-digit code
- Save to
user.sms_recovery_code - Send SMS: "Email recovery code for your dashboard: {code}"
- Return generic message (prevents user enumeration)
Step 2: Validate Code
- Find user by code + account_id
- If found:
- Remove code from user (
$unset) - Return user's email
- Remove code from user (
- Else: throw
badRequest('Invalid code')
Key Business Rules:
- Generic response prevents phone enumeration attacks
- Code is single-use (deleted after validation)
- Matches both primary and additional phones
Device Push Tokensโ
addPushToken({ user_id, token, device_id })โ
Purpose: Registers Expo push notification token for mobile device.
Parameters:
user_id(ObjectId)token(String) - Expo push tokendevice_id(String) - Unique device identifier
Returns: Promise<{ data: Object }> - Created/updated token record
Business Logic Flow:
- Upsert Token
- Find by token
- Update with user_id, device_id, updated_at
- Create if not exists
Key Business Rules:
- Upsert prevents duplicates
updated_attimestamp for staleness tracking
removePushToken({ user_id, token, device_id })โ
Purpose: Unregisters push token (e.g., user logs out of mobile app).
Parameters: Same as addPushToken
Returns: Promise<Boolean> - True if deleted, false otherwise
Business Logic Flow:
- Delete Token
- Match all three fields: user_id, token, device_id
Utility Functionsโ
profile(uid)โ
Purpose: Simple user profile retrieval by ID.
Returns: Promise<Object> - User document
getUser({ id, account_id, dashboardPreferences })โ
Purpose: Retrieves user with support for client dashboard context.
Parameters:
dashboardPreferences(Object, optional):allow_client_dashboard(Boolean)sso(Boolean)parent_account(ObjectId) - If true, uses parent account
Business Logic Flow:
-
Override Account (if dashboardPreferences)
- Use parent_account instead of account_id
-
Find User
updateAnnouncement({ id, account_id, uid, auth_user, announcement_id, is_dismissed })โ
Purpose: Marks an in-app announcement as dismissed for a user.
Parameters:
announcement_id(ObjectId) - Announcement IDis_dismissed(Boolean) - Dismissal status
Returns: Promise<Object> - Updated user
Business Logic Flow:
- Permission Check (standard)
- Update Announcement in Array
- Use
announcements.$.is_dismissedwith array filter
- Use
calendars(uid, auth)โ
Purpose: Retrieves user's calendar integrations (delegates to user model method).
Parameters:
uid(ObjectId)auth(String) - Auth token
Returns: Promise<Object> - Calendar data
Business Logic Flow:
- Find User
- Call Model Method:
user.getCalendars(auth)
๐ Integration Pointsโ
External Servicesโ
Conversation Socket Serviceโ
- Endpoint:
process.env.CONVERSATION_SOCKET/emit - Purpose: Real-time notifications for support conversation changes
- Event: 'supportUserDelete'
- Trigger: User deleted or loses conversation access
- Authentication: JWT with 2-minute expiry
SendGrid API (via External API)โ
- Purpose: Email domain validation
- Route:
/v1/e/sendgrid/subusers/domains/${id}/validate - Usage: Validate domain before adding sendgrid_email
SMS Serviceโ
- Utility:
sendSMS()from shared utilities - Purpose: Send recovery codes
- Provider: Twilio or similar (configured in env)
Email Serviceโ
- Utility:
sendEmail()from shared utilities - Template: SendGrid template
d-068220b420984ac1b734b083e248f370 - Purpose: User invitation emails with branding
Internal Dependenciesโ
shared/utilities/delete-user.new.js- Workload reassignment utilityshared/utilities/assigntoaccountsormanager.js- Project workload reassignmentshared/utilities/create-defaults.js- Create default user resources (pipelines, folders, etc.)shared/utilities/auth.js- Password hashing (newHash)shared/utilities/wasabi.js- Image URL generation for logosshared/utilities/with-transaction.js- MongoDB transaction wrapperjwt- Token generation for invitations
๐งช Edge Cases & Special Handlingโ
Case: Client Dashboard User Creationโ
Condition: Main account creates user for sub-account via client dashboard
Handling: Override account context, set metadata.software: false
Result: User has client dashboard access but not regular software access
Case: Invitation-Only Mode with Existing Userโ
Condition: Resending invitation to unverified user
Handling: Update existing user instead of creating duplicate
Validation: Throw error if user already verified
Case: Scope Change on Own Accountโ
Condition: User tries to update own scope
Handling: Throw forbidden error
Reason: Prevents privilege escalation
Case: Unavailability Outside Availability Windowโ
Condition: Setting unavailability 8AM-9AM when availability is 9AM-5PM
Handling: Throw badRequest with specific time range
Validation: Convert 12-hour to minutes, compare ranges
Case: Deleting User with Active Workloadโ
Condition: User owns deals, contacts, projects
Handling:
- If
assigned_toprovided: Reassign all data - If project role: Auto-reassign projects
- Else: Soft delete (data remains orphaned)
Case: Email Domain Not in SendGridโ
Condition: Adding email with unregistered domain
Handling: Throw notFound('No matching domain record')
Prevention: Check account.sendgrid.domains first
Case: Conversation Scope Additionโ
Condition: Granting conversation access
Handling: Auto-add user as follower to all support contacts
Performance: Uses aggregation pipeline for efficiency
โ ๏ธ Important Notesโ
- ๐จ Transaction Safety: Many operations use
withTransactionfor atomicity - ๐ Scope Security: Multiple layers of permission validation
- ๐ง Email Failures: Logged but don't block user creation
- ๐ Session Invalidation: Scope changes force re-login
- โฐ Timezone Handling: Uses moment-timezone for accurate time validation
- ๐ฑ Push Tokens: Upsert prevents duplicates
- ๐ฏ Soft Delete Default: Preserves data integrity
- ๐ฌ Conversation Integration: Deep integration with support chat system
- ๐ Workload Reassignment: Cascades across multiple collections
- โก Socket Emissions: Real-time updates to all account users
- ๐ข Client Dashboard: Separate access model from regular platform
- ๐ข Reset Token Expiry: 23.5 hours (84600 seconds)
- ๐ Associated Data: Counts used for reassignment planning
๐ Related Documentationโ
- Parent Module: Users Module
- Related Service: User Configuration
- Controller:
internal/api/v1/users/controllers/users.js - Routes:
internal/api/v1/users/routes/users.js - Utilities:
- Delete User (link removed - file does not exist)
- Workload Reassignment (link removed - file does not exist)
- Create Defaults (link removed - file does not exist)