- Added PDF certificate generation documentation - Added Docker deployment section with Dockerfile example - Added environment variables reference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
542 lines
26 KiB
Markdown
542 lines
26 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|
|
## PDF Certificate Generation
|
|
|
|
The AI Kurzy module can generate PDF certificates for course participants using Puppeteer.
|
|
|
|
**Services:**
|
|
- `certificate.service.js` — `generateCertificate(registraciaId, templateName)`, `getCertificateDownloadInfo(prilohaId)`, `getAvailableTemplates()`, `hasCertificate(registraciaId)`
|
|
- `certificate-email.service.js` — `sendCertificateEmail(registraciaId, prilohaId)` — sends certificate via JMAP
|
|
|
|
**Available Templates:**
|
|
- `AIcertifikat` — AI kurz (Zdarílek + Gablasová)
|
|
- `AIcertifikatGablas` — AI kurz (Gablas + Gablasová)
|
|
- `AIcertifikatPatrik` — AI kurz (Patrik + Gablasová)
|
|
- `ScrumMaster`, `ScrumProductOwner` — Scrum certifications
|
|
- `ITILFoundation` — ITIL® 4 Foundation
|
|
- `PRINCE2Foundation`, `PRINCE2Practitioner` — PRINCE2® certifications
|
|
|
|
---
|
|
|
|
## Docker Deployment
|
|
|
|
```dockerfile
|
|
FROM node:20-alpine
|
|
|
|
WORKDIR /app
|
|
|
|
COPY package*.json ./
|
|
RUN npm ci --only=production
|
|
|
|
# Required for PDF certificate generation
|
|
RUN apk add --no-cache chromium
|
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
|
|
|
COPY src ./src
|
|
COPY drizzle.config.js ./
|
|
|
|
RUN mkdir -p uploads
|
|
RUN addgroup -g 1001 -S nodejs && \
|
|
adduser -S nodejs -u 1001 && \
|
|
chown -R nodejs:nodejs /app
|
|
|
|
USER nodejs
|
|
EXPOSE 5000
|
|
CMD ["node", "src/index.js"]
|
|
```
|
|
|
|
**Environment Variables:**
|
|
```env
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgres://user:pass@host:5432/db
|
|
JWT_SECRET=your-secret
|
|
JWT_REFRESH_SECRET=your-refresh-secret
|
|
ENCRYPTION_KEY=32-char-key-for-aes
|
|
JMAP_URL=https://mail.truemail.sk/jmap/
|
|
VAPID_PUBLIC_KEY=...
|
|
VAPID_PRIVATE_KEY=...
|
|
CORS_ORIGIN=https://your-frontend.com
|
|
```
|