richardtekula 12acd68156 refactor: Move course dates from registracie to kurzy table
- Add datumOd and datumDo columns to kurzy table
- Remove datumOd, datumDo, pocetUcastnikov from registracie table
- Update schema, validators, and services accordingly
- Certificate generation now uses course dates
- Migration preserves existing data by copying most recent dates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 12:52:56 +01:00
2025-12-02 09:48:42 +01:00
2025-12-02 09:48:42 +01:00
2025-12-29 09:02:24 +01:00

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.jsgetAllKurzy, getKurzById, createKurz, updateKurz, deleteKurz, getKurzyStats (course CRUD + statistics)

ucastnici.service.jsgetAllUcastnici, getUcastnikById, createUcastnik, updateUcastnik, deleteUcastnik (participant CRUD)

registracie.service.jsgetAllRegistracie, 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.

Description
No description provided
Readme 5.5 MiB
Languages
JavaScript 92%
HTML 7.9%
Dockerfile 0.1%