# CRM Server

Node.js/Express backend for a full-featured CRM system with company/project management, time tracking, email integration (JMAP), internal messaging, calendar events, push notifications, and an AI courses module.

## Tech Stack

- **Runtime:** Node.js with ES modules
- **Framework:** Express.js
- **Database:** PostgreSQL via Drizzle ORM
- **Auth:** JWT (access + refresh tokens), bcrypt passwords
- **Email:** JMAP protocol
- **Files:** Multer (disk + memory storage)
- **Validation:** Zod schemas
- **Notifications:** Web Push (VAPID)
- **Exports:** ExcelJS for XLSX timesheets
- **Cron:** node-cron for scheduled tasks
- **Logging:** Winston

## Project Structure

```
src/
  config/          # Database, env, upload configs
  controllers/     # HTTP request handlers
  cron/            # Scheduled jobs
  db/              # Schema definitions, migrations, seeds
  middlewares/      # Auth, security, validation middleware
  routes/          # Express route definitions
  services/        # Business logic layer
  utils/           # Shared utilities
  validators/      # Zod validation schemas
```

---

## Config (`src/config/`)

| File | Exports |
|------|---------|
| `database.js` | `db` (Drizzle instance), `pool` (pg connection pool) |
| `env.js` | `env` object — validated env vars (DATABASE_URL, JWT_SECRET, JWT_REFRESH_SECRET, JMAP_URL, ENCRYPTION_KEY, VAPID keys, PORT) |
| `upload.js` | `createUpload()` factory for multer configs, `ALLOWED_FILE_TYPES` constant. Pre-configured exports: `upload`, `timesheetUpload`, `companyDocumentUpload`, `projectDocumentUpload`, `serviceDocumentUpload`, `aiKurzyUpload` |

---

## Middlewares (`src/middlewares/`)

### Auth
| Function | Description |
|----------|-------------|
| `authenticate` | Verify JWT from cookie or Authorization header, attach user to `req` |
| `optionalAuthenticate` | Same but doesn't fail if no token present |
| `requireAdmin` | Require admin role |
| `requireMember` | Require member role (or admin) |
| `checkResourceAccess(type)` | Check user has access to company/project/todo |
| `getAccessibleResourceIds(type, userId)` | Get IDs of resources a user can access |

### Security
| Function | Description |
|----------|-------------|
| `apiLimiter` | Rate limit: 100 req/15min |
| `authLimiter` | Rate limit: 5 req/15min (login) |
| `requireAccountId` | Validate `accountId` query param |
| `sanitizeInput(input)` | Strip XSS from user input |

### Global
| Function | Description |
|----------|-------------|
| `validateBody(schema)` | Validate `req.body` against Zod schema |
| `errorHandler` | Global error handler, formats errors consistently |
| `notFound` | 404 handler for unknown routes |

---

## Utils (`src/utils/`)

| File | Exports | Description |
|------|---------|-------------|
| `jwt.js` | `generateAccessToken`, `generateRefreshToken`, `generateTokenPair`, `verifyAccessToken`, `verifyRefreshToken` | JWT token operations (1h access, 7d refresh) |
| `password.js` | `hashPassword`, `comparePassword`, `generateTempPassword` | bcrypt hashing + temp password generation |
| `logger.js` | `logger` | Winston logger (file + console) |
| `errors.js` | `AppError`, `NotFoundError`, `ValidationError`, `AuthenticationError`, `ForbiddenError`, `ConflictError` | Custom error classes with HTTP status codes |
| `emailAccountHelper.js` | `encryptPassword`, `decryptPassword` | AES-256 encryption for stored email passwords |

---

## Validators (`src/validators/`)

| File | Schemas |
|------|---------|
| `auth.validators.js` | `loginSchema`, `setPasswordSchema`, `linkEmailSchema`, `createUserSchema` |
| `crm.validators.js` | `createCompanySchema`, `updateCompanySchema`, `createProjectSchema`, `updateProjectSchema`, `createTodoSchema`, `updateTodoSchema`, `createNoteSchema`, `updateNoteSchema`, `createReminderSchema`, `updateReminderSchema`, `startTimeEntrySchema`, `stopTimeEntrySchema`, `updateTimeEntrySchema`, `createContactSchema` |
| `email-account.validators.js` | `createEmailAccountSchema`, `updatePasswordSchema`, `toggleStatusSchema`, `signatureSchema`, `toggleSchema` |
| `service.validators.js` | `serviceIdSchema`, `folderIdSchema`, `folderDocumentIdSchema`, `createFolderSchema`, `updateFolderSchema` |
| `ai-kurzy.validators.js` | `createKurzSchema`, `updateKurzSchema`, `createUcastnikSchema`, `updateUcastnikSchema`, `createRegistraciaSchema`, `updateRegistraciaSchema`, `registracieQuerySchema`, `updateFieldSchema`, `prilohaIdSchema` |

---

## Services (`src/services/`)

### auth.service.js
| Function | Description |
|----------|-------------|
| `loginWithTempPassword(username, password, ip, ua)` | Authenticate with temp or permanent password, return JWT pair |
| `setNewPassword(userId, newPassword, auditCtx)` | Set permanent password, mark as changed |
| `linkEmail(userId, email, password, auditCtx)` | Link email account via JMAP validation |
| `skipEmailSetup(userId)` | Skip email onboarding step |
| `logout(auditCtx)` | Log logout to audit trail |
| `getUserById(userId)` | Get user with email accounts |

### admin.service.js
| Function | Description |
|----------|-------------|
| `checkUsernameExists(username)` | Check if username is taken |
| `createUser(username, firstName, lastName, role, email, password, auditCtx)` | Create user with auto-generated temp password |
| `getAllUsers()` | List all users (no passwords) |
| `getUserById(userId)` | Get user with email accounts |
| `changeUserRole(userId, newRole, auditCtx)` | Change role (admin/member) |
| `updateUser(userId, data)` | Update user name fields |
| `resetUserPassword(userId)` | Generate new temp password |
| `deleteUser(userId, auditCtx)` | Delete user and orphaned email accounts |

### company.service.js
| Function | Description |
|----------|-------------|
| `getAllCompanies(search, userId, role)` | Get companies (members see only assigned) |
| `getCompanyById(id)` | Get company with creator info |
| `createCompany(userId, data, auditCtx)` | Create company, auto-assign creator |
| `updateCompany(id, data, auditCtx)` | Update company |
| `deleteCompany(id, auditCtx)` | Delete company |
| `getCompanyWithRelations(id)` | Get with projects, todos, notes, reminders |
| `getCompanyUsers(id)` | Get assigned users |
| `assignUserToCompany(companyId, userId, addedBy, role, auditCtx)` | Add user to team |
| `removeUserFromCompany(companyId, userId, auditCtx)` | Remove from team |
| `updateUserRoleOnCompany(companyId, userId, role)` | Update team role |

### company-email.service.js
| Function | Description |
|----------|-------------|
| `getCompanyEmailThreads(companyId, userId)` | Get email threads grouped by contact |
| `getCompanyUnreadCounts(userId)` | Unread counts per company |

### company-document.service.js
| Function | Description |
|----------|-------------|
| `getDocumentsByCompanyId(id)` | List company documents |
| `uploadDocument({ companyId, userId, file, description })` | Upload document |
| `getDocumentForDownload(companyId, docId)` | Get file path for download |
| `deleteDocument(companyId, docId)` | Delete document and file |

### company-reminder.service.js
| Function | Description |
|----------|-------------|
| `getRemindersByCompanyId(id)` | Get reminders for company |
| `createReminder(companyId, data, auditCtx)` | Create reminder |
| `updateReminder(companyId, id, data, auditCtx)` | Update reminder |
| `deleteReminder(companyId, id, auditCtx)` | Delete reminder |
| `getReminderSummary(userId, role)` | Checked/unchecked stats |
| `getReminderCountsByCompany(userId, role)` | Counts grouped by company |
| `getUpcomingReminders(userId, role)` | Upcoming with due dates |

### project.service.js
| Function | Description |
|----------|-------------|
| `getAllProjects(search, companyId, userId, role)` | List with filters (members see assigned only) |
| `getProjectById(id)` | Get with creator info |
| `createProject(userId, data, auditCtx)` | Create and auto-assign creator |
| `updateProject(id, data, auditCtx)` | Update project |
| `deleteProject(id, auditCtx)` | Delete project |
| `getProjectWithRelations(id)` | Get with todos, notes, timesheets, team |
| `getProjectUsers(id)` | Get team members |
| `assignUserToProject(projectId, userId, addedBy, role, auditCtx)` | Add to team |
| `removeUserFromProject(projectId, userId, auditCtx)` | Remove from team |
| `updateUserRoleOnProject(projectId, userId, role)` | Update role |

### project-document.service.js
Same pattern as company-document: `getDocumentsByProjectId`, `uploadDocument`, `getDocumentForDownload`, `deleteDocument`.

### todo.service.js
| Function | Description |
|----------|-------------|
| `getAllTodos(filters, userId, role)` | Get with filters (project, company, status, priority, assignedTo) |
| `getTodoById(id)` | Get todo |
| `getTodoWithRelations(id)` | Get with project, company, users, notes |
| `createTodo(userId, data, auditCtx)` | Create and assign users (triggers push) |
| `updateTodo(id, data, userId, auditCtx)` | Update and manage assignments |
| `deleteTodo(id, auditCtx)` | Delete todo |
| `getOverdueCount(userId, role)` | Count overdue |
| `getCompletedByMeCount(userId)` | Count completed by user |
| `getTodoCounts(userId, role)` | Combined counts for sidebar badges |
| `markCompletedAsNotified(userId)` | Mark completed as notified |

### todo-notification.service.js
| Function | Description |
|----------|-------------|
| `notifyNewTodoAssignment(userIds, title, todoId, excludeId)` | Push notify assigned users |
| `notifyUpdatedTodoAssignment(userIds, title, todoId, excludeId)` | Push notify newly assigned users |

### note.service.js
| Function | Description |
|----------|-------------|
| `getAllNotes(filters)` | Get with filters (company, project, todo, contact) |
| `getNoteById(id)` | Get note |
| `getNotesByCompanyId(id)` | Notes for company |
| `getNotesByProjectId(id)` | Notes for project |
| `createNote(userId, data, auditCtx)` | Create note |
| `updateNote(id, data, auditCtx)` | Update note |
| `deleteNote(id, auditCtx)` | Delete note |
| `getUpcomingRemindersForUser(userId)` | Upcoming note reminders |
| `markReminderAsSent(id)` | Mark reminder sent |
| `searchNotes(term)` | Search with company/project info |

### time-tracking.service.js
| Function | Description |
|----------|-------------|
| `startTimeEntry(userId, data, auditCtx)` | Start timer (auto-stops any running entry) |
| `stopTimeEntry(entryId, userId, data, auditCtx)` | Stop and calculate duration |
| `pauseTimeEntry(entryId, userId, auditCtx)` | Pause running entry |
| `resumeTimeEntry(entryId, userId, auditCtx)` | Resume paused entry |
| `getRunningTimeEntry(userId)` | Get active entry for user |
| `getAllRunningTimeEntries()` | All active entries (admin) |
| `getAllTimeEntries(userId, filters)` | Filtered time entries |
| `getMonthlyTimeEntries(userId, year, month)` | Entries for month |
| `generateMonthlyTimesheet(userId, year, month)` | Generate XLSX timesheet |
| `generateCompanyTimesheet(userId, year, month, companyId)` | XLSX filtered by company |
| `getTimeEntryById(id)` | Get single entry |
| `getTimeEntryWithRelations(id)` | Entry with project/todo/company/user |
| `updateTimeEntry(id, userCtx, data, auditCtx)` | Update entry |
| `deleteTimeEntry(id, userCtx, auditCtx)` | Delete entry |
| `getMonthlyStats(userId, year, month)` | Monthly stats |

### timesheet.service.js
| Function | Description |
|----------|-------------|
| `uploadTimesheet({ userId, year, month, file, auditCtx })` | Upload timesheet file |
| `getTimesheetsForUser(userId, filters)` | User's timesheets |
| `getAllTimesheets(filters)` | All timesheets (admin) |
| `getDownloadInfo(id, userCtx)` | File path with access check |
| `deleteTimesheet(id, userCtx, auditCtx)` | Delete timesheet and file |

### contact.service.js
| Function | Description |
|----------|-------------|
| `getContactsForEmailAccount(accountId)` | Get contacts for email account |
| `addContact(accountId, jmapConfig, email, name, notes, userId)` | Add contact and sync JMAP emails |
| `removeContact(contactId, accountId)` | Remove contact |
| `updateContact(contactId, accountId, data)` | Update contact |
| `linkCompanyToContact(contactId, accountId, companyId, auditCtx)` | Link company |
| `unlinkCompanyFromContact(contactId, accountId)` | Unlink company |
| `createCompanyFromContact(contactId, accountId, userId, data, auditCtx)` | Create company and link |

### personal-contact.service.js
| Function | Description |
|----------|-------------|
| `listPersonalContacts(userId)` | List user's personal contacts |
| `createPersonalContact(userId, data)` | Create contact |
| `updatePersonalContact(id, userId, data)` | Update contact |
| `deletePersonalContact(id, userId)` | Delete contact |
| `getContactsByCompanyId(companyId)` | Contacts by company |

### crm-email.service.js
| Function | Description |
|----------|-------------|
| `getEmailsForAccount(accountId)` | All emails for account |
| `getEmailThread(accountId, threadId)` | Thread emails |
| `searchEmails(accountId, term)` | Search by subject/from/to/body |
| `getUnreadCountSummary(accountIds)` | Unread counts |
| `markContactEmailsAsRead(contactId, accountId)` | Mark all read for contact |
| `getContactEmailsWithUnread(accountId, contactId)` | Contact emails with status |
| `markThreadAsRead(accountId, threadId)` | Mark thread read |

### email-account.service.js
| Function | Description |
|----------|-------------|
| `getUserEmailAccounts(userId)` | Get accounts via many-to-many |
| `getEmailAccountById(accountId, userId)` | Get with access check |
| `getEmailAccountWithCredentials(accountId, userId)` | Get with decrypted password |
| `getPrimaryEmailAccount(userId)` | Get primary account |
| `createEmailAccount(userId, email, password)` | Create/link via JMAP discovery |
| `updateEmailAccountPassword(accountId, userId, password)` | Update password |
| `toggleEmailAccountStatus(accountId, userId, isActive)` | Enable/disable |
| `setPrimaryEmailAccount(accountId, userId)` | Set as primary |
| `removeUserFromEmailAccount(accountId, userId)` | Remove user access |

### email-signature.service.js
`getSignature`, `upsertSignature`, `toggleSignature`, `formatSignatureText`, `deleteSignature` — manage per-user email signatures.

### message.service.js
| Function | Description |
|----------|-------------|
| `getConversations(userId)` | All 1-on-1 conversations with unread counts |
| `getConversation(userId, partnerId)` | Messages with user, mark as read |
| `sendMessage(senderId, receiverId, content)` | Send direct message |
| `deleteConversation(userId, partnerId)` | Soft delete conversation |
| `getUnreadCount(userId)` | Total unread count |
| `getChatUsers(userId)` | All users except self |

### group.service.js
| Function | Description |
|----------|-------------|
| `createGroup(name, createdById, memberIds)` | Create group chat |
| `getUserGroups(userId)` | Groups with unread counts |
| `getGroupDetails(groupId, userId)` | Group with members |
| `getGroupMessages(groupId, userId)` | Messages, update last read |
| `sendGroupMessage(groupId, senderId, content)` | Send to group |
| `addGroupMember(groupId, userId, requesterId)` | Add member |
| `removeGroupMember(groupId, userId, requesterId)` | Remove member |
| `updateGroupName(groupId, name, userId)` | Rename group |
| `deleteGroup(groupId, userId)` | Delete (creator only) |

### push.service.js
| Function | Description |
|----------|-------------|
| `getVapidPublicKey()` | Get VAPID public key |
| `saveSubscription(userId, sub)` | Save push subscription |
| `removeSubscription(userId, endpoint)` | Remove subscription |
| `removeAllSubscriptions(userId)` | Remove all for user |
| `hasActiveSubscription(userId)` | Check if subscribed |
| `sendPushNotification(userId, payload)` | Send push to user |

### event.service.js
| Function | Description |
|----------|-------------|
| `getCalendarData(year, month, userId, isAdmin)` | Calendar events + todos |
| `getEventById(eventId, userId, isAdmin)` | Event with access check |
| `createEvent(userId, data)` | Create and assign users |
| `updateEvent(eventId, data)` | Update and reassign users |
| `deleteEvent(eventId)` | Delete event |

### audit.service.js
Provides `logLogin`, `logLogout`, `logPasswordChange`, `logEmailLink`, `logUserCreation`, `logRoleChange`, `logUserDeleted`, `logCompanyCreated`, `logCompanyUpdated`, `logCompanyDeleted`, `logCompanyUserAssigned`, `logCompanyUserRemoved`, `logProjectCreated`, `logProjectUpdated`, `logProjectDeleted`, `logProjectUserAssigned`, `logProjectUserRemoved`, and more. Each function records action, resource, details, IP, and user agent to the `auditLogs` table.

### service.service.js / service-folder.service.js / service-document.service.js
Standard CRUD for services, folders, and folder documents. Same pattern: `getAll`, `getById`, `create`, `update`, `delete`.

### ai-kurzy/ (barrel exported from ai-kurzy.service.js)

**kurzy.service.js** — `getAllKurzy`, `getKurzById`, `createKurz`, `updateKurz`, `deleteKurz`, `getKurzyStats` (course CRUD + statistics)

**ucastnici.service.js** — `getAllUcastnici`, `getUcastnikById`, `createUcastnik`, `updateUcastnik`, `deleteUcastnik` (participant CRUD)

**registracie.service.js** — `getAllRegistracie`, `getRegistraciaById`, `createRegistracia`, `updateRegistracia`, `deleteRegistracia`, `getCombinedTableData`, `updateField`, `getPrilohyByRegistracia`, `createPriloha`, `deletePriloha` (registrations + combined table view + inline field editing + attachments)

### jmap/ (email protocol integration)
| Function | Description |
|----------|-------------|
| `getJmapConfig(userId)` | Get JMAP config from user's primary email |
| `getJmapConfigFromAccount(account)` | Build JMAP config from account object |
| `discoverContactsFromJMAP(config, accountId, search, limit)` | Search and discover contacts |
| `syncEmailsFromSender(config, accountId, contactId, email, opts)` | Sync emails from sender |
| `markEmailAsRead(config, userId, jmapId, isRead)` | Mark read/unread on JMAP |
| `sendEmail(config, userId, accountId, to, subject, body, inReplyTo, threadId)` | Send via JMAP |
| `searchEmailsJMAP(config, accountId, query, limit, offset)` | Full-text JMAP search |

### status.service.js
`getServerStatus()` — returns uptime, memory usage, CPU, database connection info.

---

## Controllers (`src/controllers/`)

Controllers are thin HTTP handlers. Each extracts params/body from `req`, calls the corresponding service, and returns JSON. After the refactoring, audit logging is handled inside services via the `auditContext` parameter (`{ userId, ipAddress, userAgent }`).

| Controller | Handles |
|-----------|---------|
| `auth.controller.js` | Login, logout, refresh token, set password, link email, session |
| `admin.controller.js` | User CRUD (admin only), server status, trigger notifications |
| `company.controller.js` | Company CRUD, email threads, unread counts |
| `company-team.controller.js` | Company user assignments |
| `company-note.controller.js` | Company notes CRUD |
| `company-reminder.controller.js` | Company reminders CRUD + summary/upcoming |
| `company-document.controller.js` | Company document upload/download/delete |
| `project.controller.js` | Project CRUD, notes, team, documents |
| `project-document.controller.js` | Project document upload/download/delete |
| `todo.controller.js` | Todo CRUD, toggle, counts, overdue, my todos |
| `note.controller.js` | Note CRUD, search, reminders |
| `time-tracking.controller.js` | Start/stop/pause/resume timer, entries, stats, timesheet generation |
| `timesheet.controller.js` | Timesheet upload/download/delete |
| `contact.controller.js` | Email contacts, discover, link/create company |
| `personal-contact.controller.js` | Personal contacts CRUD |
| `crm-email.controller.js` | Email list, threads, search, sync, reply, read status |
| `email-account.controller.js` | Email account CRUD, password, status, primary |
| `email-signature.controller.js` | Signature CRUD, toggle, formatted output |
| `message.controller.js` | 1-on-1 messaging, conversations, unread count |
| `group.controller.js` | Group chat CRUD, messages, members |
| `push.controller.js` | Push subscription, VAPID key, test push |
| `event.controller.js` | Calendar events CRUD, notifications |
| `audit.controller.js` | Audit log listing and filters (admin) |
| `service.controller.js` | Services CRUD |
| `service-folder.controller.js` | Service folders CRUD |
| `service-document.controller.js` | Folder documents upload/download/delete |
| `ai-kurzy.controller.js` | Courses, participants, registrations, combined table, attachments, stats |

---

## Routes (`src/routes/`)

All routes are prefixed with `/api`. Authentication is required unless noted.

| Prefix | Methods | Description |
|--------|---------|-------------|
| `/api/auth` | POST login/logout/refresh/set-password/link-email/skip-email, GET session/me | Auth flow |
| `/api/admin` | GET/POST/PATCH/DELETE users, GET server-status, POST trigger-notifications | Admin only |
| `/api/companies` | Full CRUD + nested notes, reminders, users, documents, email-threads | Company management |
| `/api/projects` | Full CRUD + nested notes, users, documents | Project management |
| `/api/todos` | Full CRUD + /my, /counts, /toggle, /overdue-count | Task management |
| `/api/notes` | CRUD + /search, /my-reminders | Notes with reminders |
| `/api/time-tracking` | start/stop/pause/resume, entries, monthly, generate timesheets | Time tracking |
| `/api/timesheets` | Upload/download/delete, /my, /all | Timesheet files |
| `/api/contacts` | CRUD + /discover, link/unlink/create company | Email contacts |
| `/api/personal-contacts` | CRUD + /company/:companyId | Personal contacts |
| `/api/emails` | List, search, sync, reply, threads, read status | Email operations |
| `/api/email-accounts` | CRUD + password, status toggle, set-primary | Email accounts |
| `/api/email-signature` | CRUD + toggle, formatted | Email signatures |
| `/api/messages` | Conversations, send, unread, users | Direct messages |
| `/api/groups` | CRUD + messages, members | Group chat |
| `/api/push` | Subscribe/unsubscribe, VAPID key, status, test | Push notifications |
| `/api/events` | CRUD + /notify | Calendar events (admin-managed) |
| `/api/audit` | GET logs, actions, resources | Audit trail (admin) |
| `/api/services` | CRUD + folders + folder documents | Service catalog |
| `/api/ai-kurzy` | Courses, participants, registrations, combined table, attachments, stats | AI courses module |
| `/api/users` | GET all users | Basic user list |

---

## Cron Jobs (`src/cron/`)

| Job | Schedule | Description |
|-----|----------|-------------|
| `notifyUpcomingEvents` | Every 15 min | Push notifications for events starting within 30 min |
| `cleanupOldAuditLogs` | Daily 2:00 AM | Delete audit logs older than 90 days |

Manual triggers available via admin endpoints: `triggerEventNotifications()`, `triggerSingleEventNotification(eventId, adminUserId)`.

---

## Database Schema (`src/db/schema.js`)

34 tables organized by domain:

**Auth & Users:** `users`, `emailAccounts`, `userEmailAccounts`, `auditLogs`

**CRM Core:** `companies`, `companyUsers`, `companyReminders`, `companyDocuments`, `projects`, `projectUsers`, `projectDocuments`, `todos`, `todoUsers`, `notes`

**Time Tracking:** `timeEntries`, `timesheets`

**Email:** `contacts`, `personalContacts`, `emails`, `emailSignatures`

**Messaging:** `messages`, `chatGroups`, `chatGroupMembers`, `groupMessages`

**Calendar:** `events`, `eventUsers`

**Push:** `pushSubscriptions`

**Services:** `services`, `serviceFolders`, `serviceDocuments`

**AI Courses:** `kurzy`, `ucastnici`, `registracie`, `prilohy`

### Key Enums
- `role`: admin, member
- `projectStatus`: active, completed, on_hold, cancelled
- `todoStatus`: pending, in_progress, completed, cancelled
- `todoPriority`: low, medium, high, urgent
- `companyStatus`: registered, lead, customer, inactive
- `formaKurzu`: prezencne, online, hybridne
- `stavRegistracie`: potencialny, registrovany, potvrdeny, absolvoval, zruseny
- `typPrilohy`: certifikat, faktura, prihlaska, doklad_o_platbe, ine

---

## Access Control

- **Admin** — full access to all resources, user management, server status
- **Member** — sees only companies/projects they are assigned to; todos filtered accordingly
- Resource access checked via `checkResourceAccess` middleware and `getAccessibleResourceIds` helper
- Creator is auto-assigned to companies and projects on creation

## Audit Logging

All write operations log to `auditLogs` via the `auditContext` pattern. Services receive `{ userId, ipAddress, userAgent }` and call the audit service internally. Tracked: logins, logouts, password changes, CRUD on companies/projects/todos/notes/timesheets, user assignments, admin actions.
