1985 lines
47 KiB
Markdown
1985 lines
47 KiB
Markdown
# 📚 CRM SERVER - KOMPLETNÁ DOKUMENTÁCIA
|
|
|
|
> Vytvorené: 2025-11-21
|
|
> Verzia: 1.1
|
|
> Backend: Node.js + Express + Drizzle ORM + PostgreSQL
|
|
> Frontend: React
|
|
|
|
---
|
|
|
|
## 📑 OBSAH
|
|
|
|
1. [Architektúra Systému](#architektúra-systému)
|
|
2. [Services - Biznis Logika](#services)
|
|
3. [Controllers - Request Handling](#controllers)
|
|
4. [Routes - API Endpointy](#routes)
|
|
5. [Utils - Pomocné Funkcie](#utils)
|
|
6. [Kompletný Zoznam API](#kompletný-zoznam-api)
|
|
7. [Vzťahy medzi Službami](#vzťahy-medzi-službami)
|
|
|
|
---
|
|
|
|
## ARCHITEKTÚRA SYSTÉMU
|
|
|
|
### Stack
|
|
- **Backend Framework:** Express.js
|
|
- **Database:** PostgreSQL
|
|
- **ORM:** Drizzle ORM
|
|
- **Auth:** JWT (access + refresh tokens)
|
|
- **Validation:** Zod schemas
|
|
- **Email:** JMAP protocol (Truemail.sk)
|
|
- **Encryption:** AES-256-GCM (email passwords)
|
|
- **File Upload:** Multer
|
|
- **Time Tracking:** Real-time timer s automatickým stop/start
|
|
|
|
### Databázové Tabuľky
|
|
```
|
|
users → userEmailAccounts ← emailAccounts
|
|
↓ ↓
|
|
| contacts → emails
|
|
|
|
|
└─→ projectUsers ←─┐
|
|
↓ |
|
|
companies → projects → todos → notes
|
|
↓
|
|
timesheets
|
|
|
|
users → timeEntries → projects/todos/companies
|
|
```
|
|
|
|
**Vzťahy:**
|
|
- `projectUsers`: Many-to-many junction table (users ↔ projects)
|
|
- `companies` → `projects`: One-to-many (company môže mať viac projektov)
|
|
- `projects` → `todos`, `notes`, `timesheets`: One-to-many
|
|
- `users` → `todos.assignedTo`: One-to-many (user má assigned úlohy)
|
|
- `users` → `timeEntries`: One-to-many (user má záznamy času)
|
|
- `timeEntries` → `projects`, `todos`, `companies`: Optional Many-to-one
|
|
|
|
---
|
|
|
|
## SERVICES
|
|
|
|
### 1. auth.service.js
|
|
**Účel:** Autentifikácia a onboarding používateľov
|
|
|
|
**Databáza:** `users`, `userEmailAccounts`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
loginWithTempPassword(username, password, ipAddress, userAgent)
|
|
→ Overí credentials
|
|
→ Vráti JWT tokens
|
|
→ Kontroluje či treba zmeniť heslo / linknúť email
|
|
|
|
setNewPassword(userId, newPassword)
|
|
→ Hashuje heslo (bcrypt)
|
|
→ Nastaví changedPassword = true
|
|
→ Volá: password.js (hashPassword)
|
|
|
|
linkEmail(userId, email, emailPassword)
|
|
→ Volá: email.service.js (validateJmapCredentials)
|
|
→ Volá: emailAccountService.js (createEmailAccount)
|
|
→ Pripojí email k userovi
|
|
|
|
skipEmailSetup(userId)
|
|
→ Umožní preskočiť email setup
|
|
```
|
|
|
|
**Volá:**
|
|
- `emailAccountService.createEmailAccount()`
|
|
- `email.service.validateJmapCredentials()`
|
|
- `utils/password.hashPassword()`
|
|
- `utils/jwt.generateTokenPair()`
|
|
|
|
---
|
|
|
|
### 2. company.service.js
|
|
**Účel:** CRUD operácie pre firmy
|
|
|
|
**Databáza:** `companies`, `projects`, `todos`, `notes`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getAllCompanies(searchTerm)
|
|
→ SELECT * FROM companies
|
|
→ Filter: ILIKE na name, email, city
|
|
→ ORDER BY createdAt DESC
|
|
|
|
getCompanyById(companyId)
|
|
→ SELECT * FROM companies WHERE id = ?
|
|
→ Throw NotFoundError ak neexistuje
|
|
|
|
createCompany(userId, data)
|
|
→ Kontrola duplicity (name)
|
|
→ INSERT INTO companies
|
|
→ Nastaví createdBy = userId
|
|
|
|
updateCompany(companyId, data)
|
|
→ Volá getCompanyById (check existencie)
|
|
→ UPDATE companies SET ...
|
|
|
|
deleteCompany(companyId)
|
|
→ DELETE FROM companies WHERE id = ?
|
|
→ CASCADE delete: projects, todos, notes
|
|
|
|
getCompanyWithRelations(companyId)
|
|
→ Volá getCompanyById()
|
|
→ LEFT JOIN projects WHERE companyId
|
|
→ LEFT JOIN todos WHERE companyId
|
|
→ LEFT JOIN notes WHERE companyId
|
|
→ Vráti všetko v jednom objekte
|
|
```
|
|
|
|
**Volá:**
|
|
- Priamo databázu cez Drizzle ORM
|
|
- `utils/errors.NotFoundError`
|
|
- `utils/errors.ConflictError`
|
|
|
|
---
|
|
|
|
### 3. contact.service.js
|
|
**Účel:** Správa email kontaktov (per email account)
|
|
|
|
**Databáza:** `contacts`, `emails`, `companies`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getContactsForEmailAccount(emailAccountId)
|
|
→ SELECT * FROM contacts WHERE emailAccountId
|
|
|
|
addContact(emailAccountId, jmapConfig, email, name, notes, addedByUserId)
|
|
→ INSERT INTO contacts
|
|
→ Volá: jmap.service.syncEmailsFromSender()
|
|
→ Reassign: UPDATE emails SET contactId WHERE from = email
|
|
|
|
getContactById(contactId, emailAccountId)
|
|
→ Verifikuje prístup k accountu
|
|
|
|
linkCompanyToContact(contactId, emailAccountId, companyId)
|
|
→ UPDATE contacts SET companyId = ?
|
|
|
|
createCompanyFromContact(contactId, emailAccountId, userId, companyData)
|
|
→ Volá: company.service.createCompany()
|
|
→ UPDATE contacts SET companyId = newCompany.id
|
|
```
|
|
|
|
**Volá:**
|
|
- `jmap.service.syncEmailsFromSender()`
|
|
- `company.service.createCompany()`
|
|
- Databázu (contacts, emails, companies)
|
|
|
|
---
|
|
|
|
### 4. project.service.js
|
|
**Účel:** Projekty a správa tímov
|
|
|
|
**Databáza:** `projects`, `companies`, `todos`, `notes`, `timesheets`, `projectUsers`, `users`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getAllProjects(searchTerm, companyId)
|
|
→ SELECT * FROM projects
|
|
→ Filter: search (name, desc) + companyId
|
|
|
|
createProject(userId, data)
|
|
→ Validate: company exists (ak je companyId)
|
|
→ INSERT INTO projects
|
|
|
|
getProjectWithRelations(projectId)
|
|
→ SELECT project
|
|
→ LEFT JOIN company
|
|
→ LEFT JOIN todos
|
|
→ LEFT JOIN notes
|
|
→ LEFT JOIN timesheets
|
|
→ LEFT JOIN projectUsers + users (tím)
|
|
→ Vráti všetko naraz
|
|
|
|
// Team Management
|
|
getProjectUsers(projectId)
|
|
→ SELECT * FROM projectUsers
|
|
→ LEFT JOIN users
|
|
→ WHERE projectId
|
|
→ Vráti: Array<{ id, userId, role, addedBy, addedAt, user: {...} }>
|
|
→ DRIZZLE PATTERN: Používa .select() bez params, pristupuje cez row.table_name.field
|
|
|
|
assignUserToProject(projectId, userId, addedByUserId, role)
|
|
→ Validate: project exists (getProjectById)
|
|
→ Validate: user exists
|
|
→ Check: už nie je assigned (ConflictError)
|
|
→ INSERT INTO projectUsers
|
|
→ Vráti assignment s user details
|
|
→ UNIQUE constraint: projectId + userId
|
|
|
|
removeUserFromProject(projectId, userId)
|
|
→ Validate: project exists
|
|
→ Check: user je assigned (NotFoundError)
|
|
→ DELETE FROM projectUsers WHERE projectId AND userId
|
|
|
|
updateUserRoleOnProject(projectId, userId, role)
|
|
→ Validate: project exists
|
|
→ Check: user je assigned
|
|
→ UPDATE projectUsers SET role
|
|
→ Vráti updated assignment s user details
|
|
```
|
|
|
|
**Volá:**
|
|
- Databázu (projects, projectUsers, users)
|
|
- `utils/errors.NotFoundError`
|
|
- `utils/errors.ConflictError`
|
|
|
|
**Dôležité - Drizzle ORM Query Pattern:**
|
|
```javascript
|
|
// ✅ SPRÁVNE - Štandardný .select() pre joins
|
|
const rawResults = await db
|
|
.select()
|
|
.from(projectUsers)
|
|
.leftJoin(users, eq(projectUsers.userId, users.id))
|
|
.where(eq(projectUsers.projectId, projectId))
|
|
.orderBy(desc(projectUsers.addedAt));
|
|
|
|
// Potom mapovať výsledky:
|
|
const assignedUsers = rawResults.map((row) => ({
|
|
id: row.project_users.id,
|
|
userId: row.project_users.userId,
|
|
user: row.users ? {
|
|
id: row.users.id,
|
|
username: row.users.username,
|
|
} : null,
|
|
}));
|
|
|
|
// ❌ NESPRÁVNE - Nested object syntax NEFUNGUJE v Drizzle!
|
|
const wrong = await db
|
|
.select({
|
|
user: {
|
|
id: users.id,
|
|
username: users.username,
|
|
}
|
|
})
|
|
.from(projectUsers)
|
|
// → TypeError: Cannot convert undefined or null to object
|
|
```
|
|
|
|
---
|
|
|
|
### 5. todo.service.js
|
|
**Účel:** Správa úloh (tasks)
|
|
|
|
**Databáza:** `todos`, `projects`, `companies`, `users`, `notes`
|
|
|
|
**Status Enum:** `['pending', 'in_progress', 'completed', 'cancelled']`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getAllTodos(filters)
|
|
→ Filtre: search, projectId, companyId, assignedTo, status
|
|
→ SELECT * FROM todos WHERE ...
|
|
|
|
createTodo(userId, data)
|
|
→ Validate: project/company/assignedTo exists
|
|
→ INSERT INTO todos
|
|
→ Default status: 'pending'
|
|
|
|
updateTodo(todoId, data)
|
|
→ Ak status = 'completed' → nastaví completedAt = NOW()
|
|
→ Ak status != 'completed' → zmaže completedAt
|
|
→ UPDATE todos
|
|
|
|
toggleTodoComplete(todoId)
|
|
→ Toggle: 'completed' ↔ 'pending'
|
|
→ Používa sa vo frontende pre checkbox toggle
|
|
|
|
getTodoWithRelations(todoId)
|
|
→ LEFT JOIN project
|
|
→ LEFT JOIN company
|
|
→ LEFT JOIN assignedUser (users)
|
|
→ LEFT JOIN notes
|
|
```
|
|
|
|
**Volá:**
|
|
- Databázu (todos, projects, companies, users)
|
|
- `utils/errors.NotFoundError`
|
|
|
|
**⚠️ DÔLEŽITÉ - Status Field:**
|
|
- Databázové pole: `status` (enum)
|
|
- **NIE** `completed` (boolean)!
|
|
- Frontend check: `todo.status === 'completed'` ✅
|
|
- Frontend check: `todo.completed` ❌ (toto pole neexistuje!)
|
|
|
|
---
|
|
|
|
### 6. note.service.js
|
|
**Účel:** Poznámky s remindermi
|
|
|
|
**Databáza:** `notes`, `companies`, `projects`, `todos`, `contacts`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
// Frontend Mapping Helper
|
|
mapNoteForFrontend(note)
|
|
→ Konvertuje: reminderDate → reminderAt
|
|
→ Frontend používa "reminderAt", DB má "reminderDate"
|
|
|
|
getAllNotes(filters)
|
|
→ Filter: search, companyId, projectId, todoId, contactId
|
|
→ Volá mapNoteForFrontend() na výsledky
|
|
|
|
createNote(userId, data)
|
|
→ Validate: company/project/todo/contact exists
|
|
→ INSERT INTO notes
|
|
→ reminderSent = false
|
|
→ Volá mapNoteForFrontend()
|
|
|
|
updateNote(noteId, data)
|
|
→ Ak sa zmení reminderDate → reset reminderSent = false
|
|
→ Volá mapNoteForFrontend()
|
|
|
|
// Reminder Management
|
|
getPendingReminders()
|
|
→ SELECT * WHERE reminderDate <= NOW() AND reminderSent = false
|
|
|
|
markReminderAsSent(noteId)
|
|
→ UPDATE notes SET reminderSent = true
|
|
|
|
getUpcomingRemindersForUser(userId)
|
|
→ WHERE createdBy = userId AND pending reminders
|
|
```
|
|
|
|
**Volá:**
|
|
- Databázu (notes, companies, projects, todos, contacts)
|
|
- `mapNoteForFrontend()` - internal helper
|
|
|
|
---
|
|
|
|
### 7. email-account.service.js
|
|
**Účel:** Správa email účtov (many-to-many sharing)
|
|
|
|
**Databáza:** `emailAccounts`, `userEmailAccounts`, `users`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getUserEmailAccounts(userId)
|
|
→ SELECT emailAccounts
|
|
→ JOIN userEmailAccounts
|
|
→ WHERE userId
|
|
|
|
createEmailAccount(userId, email, emailPassword)
|
|
→ Volá: email.service.validateJmapCredentials()
|
|
→ Encrypt password: password.encryptPassword()
|
|
→ Kontrola: email už existuje?
|
|
- Áno → link existujúci account (shared = true)
|
|
- Nie → vytvor nový account
|
|
→ INSERT INTO userEmailAccounts
|
|
→ Ak prvý account → set isPrimary = true
|
|
→ Volá: contact.service.syncContactsForAccount()
|
|
|
|
getEmailAccountWithCredentials(accountId, userId)
|
|
→ SELECT emailAccount
|
|
→ Decrypt password: password.decryptPassword()
|
|
→ Vráti s plaintext password (pre JMAP)
|
|
|
|
setPrimaryEmailAccount(accountId, userId)
|
|
→ UPDATE userEmailAccounts SET isPrimary = false (všetky)
|
|
→ UPDATE userEmailAccounts SET isPrimary = true (tento)
|
|
|
|
removeUserFromEmailAccount(accountId, userId)
|
|
→ DELETE FROM userEmailAccounts
|
|
→ Ak posledný user → CASCADE delete emailAccount + data
|
|
|
|
shareEmailAccountWithUser(accountId, ownerId, targetUserId)
|
|
→ Kontrola: owner má prístup
|
|
→ INSERT INTO userEmailAccounts (link target user)
|
|
```
|
|
|
|
**Volá:**
|
|
- `email.service.validateJmapCredentials()`
|
|
- `password.encryptPassword()`
|
|
- `password.decryptPassword()`
|
|
- `contact.service.syncContactsForAccount()`
|
|
- Databázu (emailAccounts, userEmailAccounts)
|
|
|
|
---
|
|
|
|
### 8. crm-email.service.js
|
|
**Účel:** Správa emailov (read status, search)
|
|
|
|
**Databáza:** `emails`, `contacts`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
getEmailsForAccount(emailAccountId)
|
|
→ SELECT emails
|
|
→ LEFT JOIN contacts
|
|
→ WHERE emailAccountId
|
|
→ Iba emaily od pridaných kontaktov
|
|
|
|
getEmailThread(emailAccountId, threadId)
|
|
→ SELECT emails WHERE threadId
|
|
→ ORDER BY date ASC (konverzácia)
|
|
|
|
searchEmails(emailAccountId, query)
|
|
→ DB search: ILIKE na subject, body, from
|
|
→ LIMIT 50
|
|
|
|
getUnreadCountForAccount(emailAccountId)
|
|
→ SELECT COUNT WHERE isRead = false
|
|
→ Iba od added contacts
|
|
|
|
markThreadAsRead(emailAccountId, threadId)
|
|
→ UPDATE emails SET isRead = true WHERE threadId
|
|
|
|
markContactEmailsAsRead(contactId, emailAccountId)
|
|
→ Volá: getContactByEmail()
|
|
→ UPDATE emails SET isRead = true WHERE contactId
|
|
```
|
|
|
|
**Volá:**
|
|
- Databázu (emails, contacts)
|
|
- `getContactByEmail()` - internal
|
|
|
|
---
|
|
|
|
### 9. jmap.service.js
|
|
**Účel:** JMAP protokol integrácia
|
|
|
|
**JMAP Server:** `https://mail.truemail.sk/jmap/`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
jmapRequest(jmapConfig, methodCalls)
|
|
→ HTTP POST na JMAP server
|
|
→ Authorization: Basic + password
|
|
→ Vráti raw JMAP response
|
|
|
|
getMailboxes(jmapConfig)
|
|
→ JMAP call: "Mailbox/get"
|
|
→ Vráti Inbox, Sent, Trash, atď.
|
|
|
|
syncEmailsFromSender(jmapConfig, emailAccountId, contactId, email)
|
|
→ JMAP query: "Email/query" WHERE from = email
|
|
→ Fetch email details: "Email/get"
|
|
→ INSERT INTO emails (bulk)
|
|
→ Volá: markEmailAsRead() na serveri
|
|
|
|
searchEmailsJMAP(jmapConfig, emailAccountId, query)
|
|
→ JMAP full-text search
|
|
→ Neukláda do DB, iba vráti results
|
|
|
|
markEmailAsRead(jmapConfig, jmapId, isRead)
|
|
→ JMAP: "Email/set" update \Seen flag
|
|
→ Sync: UPDATE local DB
|
|
|
|
sendEmail(jmapConfig, to, subject, body, inReplyTo, threadId)
|
|
→ JMAP: "EmailSubmission/set"
|
|
→ Pošle email cez JMAP server
|
|
→ INSERT do local DB (sentByUserId)
|
|
|
|
discoverContactsFromJMAP(jmapConfig, emailAccountId, search, limit)
|
|
→ JMAP: "Email/query" group by sender
|
|
→ Vráti unique senderi (potenciálne kontakty)
|
|
```
|
|
|
|
**Volá:**
|
|
- JMAP server API (HTTP POST)
|
|
- Databázu (emails) pre sync
|
|
|
|
---
|
|
|
|
### 10. email.service.js
|
|
**Účel:** Validácia JMAP credentials
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
validateJmapCredentials(email, password)
|
|
→ HTTP GET na JMAP server /.well-known/jmap
|
|
→ Authorization: Basic email:password
|
|
→ Ak 200 → credentials OK
|
|
→ Ak 401 → invalid credentials
|
|
→ Vráti accountId a session data
|
|
```
|
|
|
|
**Volá:**
|
|
- JMAP server (validácia)
|
|
|
|
---
|
|
|
|
### 11. time-tracking.service.js
|
|
**Účel:** Sledovanie odpracovaného času
|
|
|
|
**Databáza:** `timeEntries`, `projects`, `todos`, `companies`, `users`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
startTimeEntry(userId, data)
|
|
→ Validate: project/todo/company exists (ak sú poskytnuté)
|
|
→ Check: existujúci bežiaci časovač
|
|
→ Ak beží → automaticky ho zastaví
|
|
→ INSERT INTO timeEntries
|
|
→ isRunning = true, startTime = NOW()
|
|
|
|
stopTimeEntry(entryId, userId, data)
|
|
→ Validate: ownership, isRunning = true
|
|
→ Počíta duration v minútach
|
|
→ UPDATE timeEntries SET endTime, duration, isRunning = false
|
|
→ Optional: update projectId, todoId, companyId, description
|
|
|
|
getRunningTimeEntry(userId)
|
|
→ SELECT * WHERE userId AND isRunning = true
|
|
→ Vráti aktuálny bežiaci časovač (alebo null)
|
|
|
|
getAllTimeEntries(userId, filters)
|
|
→ Filter: projectId, todoId, companyId, startDate, endDate
|
|
→ SELECT * FROM timeEntries WHERE userId
|
|
→ ORDER BY startTime DESC
|
|
|
|
getMonthlyTimeEntries(userId, year, month)
|
|
→ SELECT * WHERE userId AND startTime BETWEEN month range
|
|
→ Používa sa pre mesačný prehľad
|
|
|
|
getTimeEntryById(entryId)
|
|
→ SELECT * WHERE id
|
|
→ Throw NotFoundError ak neexistuje
|
|
|
|
getTimeEntryWithRelations(entryId)
|
|
→ LEFT JOIN project, todo, company
|
|
→ Vráti všetko v jednom objekte
|
|
|
|
updateTimeEntry(entryId, userId, data)
|
|
→ Validate: ownership, NOT isRunning (nemožno upraviť bežiaci)
|
|
→ Update: startTime, endTime, projectId, todoId, companyId, description
|
|
→ Prepočíta duration ak sa zmení čas
|
|
→ Set isEdited = true
|
|
|
|
deleteTimeEntry(entryId, userId)
|
|
→ Validate: ownership, NOT isRunning
|
|
→ DELETE FROM timeEntries
|
|
|
|
getMonthlyStats(userId, year, month)
|
|
→ Volá getMonthlyTimeEntries()
|
|
→ Počíta štatistiky:
|
|
- totalMinutes / totalHours
|
|
- daysWorked (unique days)
|
|
- averagePerDay
|
|
- byProject (čas per projekt)
|
|
- byCompany (čas per firma)
|
|
```
|
|
|
|
**Volá:**
|
|
- Databázu (timeEntries, projects, todos, companies)
|
|
- `utils/errors.NotFoundError`
|
|
- `utils/errors.BadRequestError`
|
|
|
|
**Dôležité poznámky:**
|
|
- **Auto-stop:** Pri štarte nového časovača sa automaticky zastaví predchádzajúci
|
|
- **Duration:** Ukladá sa v minútach (Math.round)
|
|
- **isEdited flag:** Označuje manuálne upravené záznamy
|
|
- **isRunning:** Iba jeden časovač môže byť aktívny pre usera
|
|
|
|
---
|
|
|
|
### 12. audit.service.js
|
|
**Účel:** Audit logging pre compliance
|
|
|
|
**Databáza:** `auditLogs`
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
logAuditEvent(userId, action, resourceType, resourceId, details, ip, userAgent)
|
|
→ INSERT INTO auditLogs
|
|
→ Ukladá všetky parametre
|
|
|
|
// Špecifické loggery
|
|
logLoginAttempt(username, success, ip, userAgent, error)
|
|
→ action = "login_attempt"
|
|
|
|
logPasswordChange(userId, ip, userAgent)
|
|
→ action = "password_change"
|
|
|
|
logEmailLink(userId, email, ip, userAgent)
|
|
→ action = "email_linked"
|
|
|
|
logRoleChange(adminId, userId, oldRole, newRole, ip, userAgent)
|
|
→ action = "role_change"
|
|
|
|
logUserCreation(adminId, newUserId, username, role, ip, userAgent)
|
|
→ action = "user_created"
|
|
```
|
|
|
|
**Volá:**
|
|
- Iba databázu (write-only)
|
|
|
|
---
|
|
|
|
## CONTROLLERS
|
|
|
|
**Účel:** Spracovanie HTTP requestov, volanie services, vracanie responses
|
|
|
|
### Štruktúra každého controllera:
|
|
```javascript
|
|
export const methodName = async (req, res) => {
|
|
try {
|
|
// 1. Extract params/body
|
|
const { param } = req.params
|
|
const data = req.body
|
|
const userId = req.userId // z authenticate middleware
|
|
|
|
// 2. Volaj service
|
|
const result = await service.method(param, data)
|
|
|
|
// 3. Vráť response
|
|
res.status(200).json({
|
|
success: true,
|
|
data: result,
|
|
message: 'Success message'
|
|
})
|
|
} catch (error) {
|
|
// 4. Error handling
|
|
const errorResponse = formatErrorResponse(error, isDev)
|
|
res.status(error.statusCode || 500).json(errorResponse)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Response Format (štandard):
|
|
```json
|
|
{
|
|
"success": true/false,
|
|
"data": { ... },
|
|
"count": 10, // optional (pre lists)
|
|
"message": "Success/Error message"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ROUTES
|
|
|
|
**Účel:** Definícia endpointov, middleware, validácia
|
|
|
|
### Štruktúra route file:
|
|
```javascript
|
|
import express from 'express'
|
|
import * as controller from '../controllers/xxx.controller.js'
|
|
import { authenticate } from '../middlewares/auth/authMiddleware.js'
|
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js'
|
|
import { schema } from '../validators/xxx.validators.js'
|
|
|
|
const router = express.Router()
|
|
|
|
// Global middleware (pre všetky routes)
|
|
router.use(authenticate)
|
|
|
|
// Route definition
|
|
router.get('/',
|
|
controller.getAll
|
|
)
|
|
|
|
router.post('/',
|
|
validateBody(schema), // Zod validation
|
|
controller.create
|
|
)
|
|
|
|
router.patch('/:id',
|
|
validateParams(z.object({ id: z.string().uuid() })),
|
|
validateBody(updateSchema),
|
|
controller.update
|
|
)
|
|
|
|
export default router
|
|
```
|
|
|
|
### Middleware poradie:
|
|
1. `authenticate` - JWT validácia
|
|
2. `validateParams` - Path params validácia (Zod)
|
|
3. `validateBody` - Request body validácia (Zod)
|
|
4. `controller.method` - Controller handler
|
|
|
|
---
|
|
|
|
## UTILS
|
|
|
|
### 1. errors.js
|
|
**Účel:** Custom error classy a formatting
|
|
|
|
**Error Classes:**
|
|
```javascript
|
|
class AppError extends Error {
|
|
constructor(message, statusCode) {
|
|
this.statusCode = statusCode
|
|
this.isOperational = true
|
|
}
|
|
}
|
|
|
|
class ValidationError extends AppError { statusCode = 400 }
|
|
class BadRequestError extends AppError { statusCode = 400 }
|
|
class AuthenticationError extends AppError { statusCode = 401 }
|
|
class ForbiddenError extends AppError { statusCode = 403 }
|
|
class NotFoundError extends AppError { statusCode = 404 }
|
|
class ConflictError extends AppError { statusCode = 409 }
|
|
class RateLimitError extends AppError { statusCode = 429 }
|
|
```
|
|
|
|
**Funkcie:**
|
|
```javascript
|
|
formatErrorResponse(error, includeStack)
|
|
→ Formátuje error pre API response
|
|
→ V dev mode: vracia stack trace
|
|
→ V prod mode: iba message
|
|
```
|
|
|
|
**Použitie:**
|
|
```javascript
|
|
throw new NotFoundError('Company not found')
|
|
throw new ConflictError('Email already exists')
|
|
```
|
|
|
|
---
|
|
|
|
### 2. jwt.js
|
|
**Účel:** JWT token management
|
|
|
|
**Funkcie:**
|
|
```javascript
|
|
generateAccessToken(payload)
|
|
→ jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' })
|
|
|
|
generateRefreshToken(payload)
|
|
→ jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: '7d' })
|
|
|
|
verifyAccessToken(token)
|
|
→ jwt.verify(token, JWT_SECRET)
|
|
→ Vráti decoded payload alebo throw error
|
|
|
|
generateTokenPair(user)
|
|
→ Vytvorí obe tokens naraz
|
|
→ Payload: { userId, username, role }
|
|
```
|
|
|
|
**Environment Variables:**
|
|
```bash
|
|
JWT_SECRET=xxx
|
|
JWT_REFRESH_SECRET=xxx
|
|
JWT_EXPIRES_IN=1h
|
|
JWT_REFRESH_EXPIRES_IN=7d
|
|
```
|
|
|
|
---
|
|
|
|
### 3. logger.js
|
|
**Účel:** Farebné console logging
|
|
|
|
**Metódy:**
|
|
```javascript
|
|
logger.info(message, ...args) // 🔵 Blue
|
|
logger.success(message, ...args) // 🟢 Green
|
|
logger.warn(message, ...args) // 🟡 Yellow
|
|
logger.error(message, error) // 🔴 Red
|
|
logger.debug(message, ...args) // 🔷 Cyan (dev only)
|
|
logger.audit(message, ...args) // 🟣 Magenta
|
|
```
|
|
|
|
**Použitie:**
|
|
```javascript
|
|
logger.info('Server started', { port: 5000 })
|
|
logger.success('User created', { userId })
|
|
logger.error('Database error', error)
|
|
```
|
|
|
|
---
|
|
|
|
### 4. password.js
|
|
**Účel:** Password hashing, encryption, generation
|
|
|
|
**Funkcie:**
|
|
```javascript
|
|
// Bcrypt (pre user passwords)
|
|
hashPassword(password)
|
|
→ bcrypt.hash(password, BCRYPT_ROUNDS)
|
|
|
|
comparePassword(password, hash)
|
|
→ bcrypt.compare(password, hash)
|
|
|
|
// AES-256-GCM (pre email passwords)
|
|
encryptPassword(text)
|
|
→ crypto.randomBytes(16) // IV
|
|
→ crypto.scrypt(JWT_SECRET) // Key derivation
|
|
→ cipher = crypto.createCipheriv('aes-256-gcm')
|
|
→ Vráti: "iv:authTag:encrypted" (base64)
|
|
|
|
decryptPassword(encryptedText)
|
|
→ Split "iv:authTag:encrypted"
|
|
→ decipher = crypto.createDecipheriv('aes-256-gcm')
|
|
→ Vráti plaintext
|
|
|
|
// Temp password generation
|
|
generateTempPassword(length = 12)
|
|
→ Random: uppercase + lowercase + numbers + special chars
|
|
→ Použitie: admin vytvára usera
|
|
```
|
|
|
|
**Prečo 2 typy encryption?**
|
|
- **Bcrypt (user passwords):** One-way hash, nemožné dekryptovať
|
|
- **AES-256 (email passwords):** Reversible, treba plaintext pre JMAP login
|
|
|
|
---
|
|
|
|
## KOMPLETNÝ ZOZNAM API
|
|
|
|
### 🔐 AUTHENTICATION
|
|
|
|
#### POST /api/auth/login
|
|
```
|
|
Účel: Prihlásenie používateľa
|
|
Body: { username, password }
|
|
Response: { user, tokens, needsPasswordChange, needsEmailSetup }
|
|
Rate Limit: Áno
|
|
```
|
|
|
|
#### POST /api/auth/set-password
|
|
```
|
|
Účel: Zmena hesla pri prvom prihlásení
|
|
Body: { newPassword }
|
|
Auth: Áno
|
|
Rate Limit: Áno
|
|
```
|
|
|
|
#### POST /api/auth/link-email
|
|
```
|
|
Účel: Pripojenie email účtu
|
|
Body: { email, emailPassword }
|
|
Auth: Áno
|
|
Volá: email.service, emailAccountService
|
|
```
|
|
|
|
#### POST /api/auth/skip-email
|
|
```
|
|
Účel: Preskočiť email setup
|
|
Auth: Áno
|
|
```
|
|
|
|
#### POST /api/auth/logout
|
|
```
|
|
Účel: Odhlásenie (clear cookies)
|
|
Auth: Áno
|
|
Response: Clear accessToken & refreshToken cookies
|
|
```
|
|
|
|
#### GET /api/auth/session
|
|
```
|
|
Účel: Získať info o aktuálnej session
|
|
Auth: Áno
|
|
Response: { user, authenticated: true }
|
|
```
|
|
|
|
#### GET /api/auth/me
|
|
```
|
|
Účel: Profil aktuálneho usera
|
|
Auth: Áno
|
|
Response: { user with emailAccounts }
|
|
```
|
|
|
|
---
|
|
|
|
### 👥 ADMIN (User Management)
|
|
|
|
#### POST /api/admin/users
|
|
```
|
|
Účel: Vytvoriť nového usera
|
|
Body: { username, email?, emailPassword?, firstName?, lastName? }
|
|
Auth: Áno (admin only)
|
|
Response: { user with tempPassword }
|
|
Efekt: Vygeneruje temp password, user musí zmeniť pri login
|
|
```
|
|
|
|
#### GET /api/admin/users
|
|
```
|
|
Účel: Zoznam všetkých userov
|
|
Auth: Áno (admin only)
|
|
Response: Array of users
|
|
```
|
|
|
|
#### GET /api/admin/users/:userId
|
|
```
|
|
Účel: Detail usera
|
|
Auth: Áno (admin only)
|
|
Response: { user with emailAccounts }
|
|
```
|
|
|
|
#### PATCH /api/admin/users/:userId/role
|
|
```
|
|
Účel: Zmeniť rolu usera
|
|
Body: { role: "admin" | "member" }
|
|
Auth: Áno (admin only)
|
|
```
|
|
|
|
#### DELETE /api/admin/users/:userId
|
|
```
|
|
Účel: Zmazať usera
|
|
Auth: Áno (admin only)
|
|
Efekt: CASCADE delete všetkých dát usera
|
|
```
|
|
|
|
---
|
|
|
|
### 🏢 COMPANIES
|
|
|
|
#### GET /api/companies?search=query
|
|
```
|
|
Účel: Zoznam firiem
|
|
Query: search (optional) - hľadá v name, email, city
|
|
Auth: Áno
|
|
Response: Array of companies
|
|
```
|
|
|
|
#### GET /api/companies/:companyId
|
|
```
|
|
Účel: Detail firmy
|
|
Auth: Áno
|
|
Response: Company object
|
|
```
|
|
|
|
#### GET /api/companies/:companyId/details
|
|
```
|
|
Účel: Firma s všetkými reláciami
|
|
Auth: Áno
|
|
Response: { ...company, projects: [], todos: [], notes: [] }
|
|
Poznámka: NEVYUŽÍVA SA vo frontende (robí sa 3 samostatné cally)
|
|
```
|
|
|
|
#### POST /api/companies
|
|
```
|
|
Účel: Vytvoriť firmu
|
|
Body: { name*, description, address, city, country, phone, email, website }
|
|
Auth: Áno
|
|
Validation: createCompanySchema
|
|
```
|
|
|
|
#### PATCH /api/companies/:companyId
|
|
```
|
|
Účel: Upraviť firmu
|
|
Body: (all optional) { name, description, ... }
|
|
Auth: Áno
|
|
Validation: updateCompanySchema
|
|
```
|
|
|
|
#### DELETE /api/companies/:companyId
|
|
```
|
|
Účel: Zmazať firmu
|
|
Auth: Áno
|
|
Efekt: CASCADE delete projects, todos, notes
|
|
```
|
|
|
|
#### GET /api/companies/:companyId/notes
|
|
```
|
|
Účel: Poznámky firmy
|
|
Auth: Áno
|
|
Response: Array of notes (mapped reminderDate→reminderAt)
|
|
```
|
|
|
|
#### POST /api/companies/:companyId/notes
|
|
```
|
|
Účel: Pridať poznámku k firme
|
|
Body: { content*, reminderAt? }
|
|
Auth: Áno
|
|
```
|
|
|
|
#### PATCH /api/companies/:companyId/notes/:noteId
|
|
```
|
|
Účel: Upraviť poznámku firmy
|
|
Body: { content?, reminderAt? }
|
|
Auth: Áno
|
|
```
|
|
|
|
#### DELETE /api/companies/:companyId/notes/:noteId
|
|
```
|
|
Účel: Zmazať poznámku firmy
|
|
Auth: Áno
|
|
```
|
|
|
|
---
|
|
|
|
### 📁 PROJECTS
|
|
|
|
#### GET /api/projects?search=query&companyId=uuid
|
|
```
|
|
Účel: Zoznam projektov
|
|
Query: search, companyId (both optional)
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/projects/:projectId
|
|
```
|
|
Účel: Detail projektu
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/projects/:projectId/details
|
|
```
|
|
Účel: Projekt s reláciami
|
|
Auth: Áno
|
|
Response: { ...project, company, todos, notes, timesheets, assignedUsers }
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/projects
|
|
```
|
|
Účel: Vytvoriť projekt
|
|
Body: { name*, description, companyId, status, startDate, endDate }
|
|
Auth: Áno
|
|
Validation: createProjectSchema
|
|
```
|
|
|
|
#### PATCH /api/projects/:projectId
|
|
```
|
|
Účel: Upraviť projekt
|
|
Body: (all optional)
|
|
Auth: Áno
|
|
Validation: updateProjectSchema
|
|
```
|
|
|
|
#### DELETE /api/projects/:projectId
|
|
```
|
|
Účel: Zmazať projekt
|
|
Auth: Áno
|
|
Efekt: CASCADE delete todos, notes, projectUsers
|
|
```
|
|
|
|
#### GET /api/projects/:projectId/notes
|
|
```
|
|
Účel: Poznámky projektu
|
|
Auth: Áno
|
|
```
|
|
|
|
#### POST /api/projects/:projectId/notes
|
|
```
|
|
Účel: Pridať poznámku k projektu
|
|
Body: { content*, reminderAt? }
|
|
Auth: Áno
|
|
```
|
|
|
|
#### PATCH /api/projects/:projectId/notes/:noteId
|
|
```
|
|
Účel: Upraviť poznámku projektu
|
|
Auth: Áno
|
|
```
|
|
|
|
#### DELETE /api/projects/:projectId/notes/:noteId
|
|
```
|
|
Účel: Zmazať poznámku projektu
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/projects/:projectId/users
|
|
```
|
|
Účel: Členovia tímu projektu (many-to-many relationship)
|
|
Auth: Áno
|
|
Response: Array of {
|
|
id,
|
|
userId,
|
|
role,
|
|
addedBy,
|
|
addedAt,
|
|
user: { id, username, email, role }
|
|
}
|
|
Volá: project.service.getProjectUsers()
|
|
```
|
|
|
|
#### POST /api/projects/:projectId/users
|
|
```
|
|
Účel: Priradiť usera k projektu
|
|
Body: { userId*, role? }
|
|
Auth: Áno
|
|
Validation:
|
|
- userId musí byť UUID
|
|
- role je optional (text)
|
|
- User nesmie byť už assigned (UNIQUE constraint)
|
|
Response: Assignment with user details
|
|
Volá: project.service.assignUserToProject()
|
|
Errors:
|
|
- 404: Project alebo User nenájdený
|
|
- 409: User už je assigned k projektu
|
|
```
|
|
|
|
#### PATCH /api/projects/:projectId/users/:userId
|
|
```
|
|
Účel: Zmeniť rolu usera na projekte
|
|
Params: projectId (UUID), userId (UUID)
|
|
Body: { role? }
|
|
Auth: Áno
|
|
Poznámka: Momentálne sa NEVYUŽÍVA vo frontende (role field removed)
|
|
Volá: project.service.updateUserRoleOnProject()
|
|
```
|
|
|
|
#### DELETE /api/projects/:projectId/users/:userId
|
|
```
|
|
Účel: Odstrániť usera z projektu
|
|
Params: projectId (UUID), userId (UUID)
|
|
Auth: Áno
|
|
Response: { success: true, message: 'Používateľ bol odstránený z projektu' }
|
|
Volá: project.service.removeUserFromProject()
|
|
Errors:
|
|
- 404: User nie je assigned k projektu
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ TODOS
|
|
|
|
#### GET /api/todos?search=&projectId=&companyId=&assignedTo=&status=
|
|
```
|
|
Účel: Zoznam úloh
|
|
Query: všetky parametre optional
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/todos/my?status=
|
|
```
|
|
Účel: Moje úlohy (assigned to current user)
|
|
Auth: Áno
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### GET /api/todos/:todoId
|
|
```
|
|
Účel: Detail todo
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/todos/:todoId/details
|
|
```
|
|
Účel: Todo s reláciami
|
|
Auth: Áno
|
|
Response: { ...todo, project, company, assignedUser, notes }
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/todos
|
|
```
|
|
Účel: Vytvoriť todo
|
|
Body: { title*, description, projectId, companyId, assignedTo, status, priority, dueDate }
|
|
Auth: Áno
|
|
Validation: createTodoSchema
|
|
```
|
|
|
|
#### PATCH /api/todos/:todoId
|
|
```
|
|
Účel: Upraviť todo
|
|
Body: (all optional)
|
|
Auth: Áno
|
|
Efekt: Ak status=completed → nastaví completedAt
|
|
```
|
|
|
|
#### DELETE /api/todos/:todoId
|
|
```
|
|
Účel: Zmazať todo
|
|
Auth: Áno
|
|
```
|
|
|
|
#### PATCH /api/todos/:todoId/toggle
|
|
```
|
|
Účel: Toggle completed status
|
|
Auth: Áno
|
|
Efekt: status 'completed' ↔ 'pending'
|
|
Poznámka: Používa sa vo frontende pre checkbox toggle
|
|
Response: Updated todo
|
|
```
|
|
|
|
---
|
|
|
|
### 📝 NOTES (Standalone)
|
|
|
|
**POZNÁMKA:** Všetky standalone note routes sú **NEVYUŽITÉ** vo frontende.
|
|
Notes sa používajú iba cez nested routes (companies/:id/notes, projects/:id/notes).
|
|
|
|
#### GET /api/notes?search=&companyId=&projectId=&todoId=&contactId=
|
|
```
|
|
Účel: Zoznam všetkých poznámok
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
#### GET /api/notes/my-reminders
|
|
```
|
|
Účel: Moje pending reminders
|
|
Poznámka: NEVYUŽÍVA SA (mohlo by byť užitočné!)
|
|
```
|
|
|
|
#### GET /api/notes/:noteId
|
|
```
|
|
Účel: Detail poznámky
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
#### POST /api/notes
|
|
```
|
|
Účel: Vytvoriť standalone poznámku
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
#### PATCH /api/notes/:noteId
|
|
```
|
|
Účel: Upraviť poznámku
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
#### DELETE /api/notes/:noteId
|
|
```
|
|
Účel: Zmazať poznámku
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
#### POST /api/notes/:noteId/mark-reminder-sent
|
|
```
|
|
Účel: Označiť reminder ako odoslaný
|
|
Poznámka: NEVYUŽÍVA SA
|
|
```
|
|
|
|
---
|
|
|
|
### 👤 CONTACTS
|
|
|
|
#### GET /api/contacts?accountId=uuid
|
|
```
|
|
Účel: Zoznam kontaktov pre email account
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/contacts/discover?accountId=&search=&limit=
|
|
```
|
|
Účel: Objaviť potenciálne kontakty z emailov
|
|
Query: accountId (optional, uses primary), search, limit
|
|
Auth: Áno
|
|
Volá: jmap.service.discoverContactsFromJMAP()
|
|
```
|
|
|
|
#### POST /api/contacts
|
|
```
|
|
Účel: Pridať kontakt
|
|
Body: { email*, name, notes, accountId }
|
|
Auth: Áno
|
|
Efekt: Sync emails from sender, reassign existing emails
|
|
Volá: jmap.service.syncEmailsFromSender()
|
|
```
|
|
|
|
#### PATCH /api/contacts/:contactId?accountId=uuid
|
|
```
|
|
Účel: Upraviť kontakt
|
|
Body: { name, notes }
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
```
|
|
|
|
#### DELETE /api/contacts/:contactId?accountId=uuid
|
|
```
|
|
Účel: Zmazať kontakt
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
Efekt: CASCADE delete emails
|
|
```
|
|
|
|
#### POST /api/contacts/:contactId/link-company?accountId=uuid
|
|
```
|
|
Účel: Linknúť firmu k kontaktu
|
|
Body: { companyId* }
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/contacts/:contactId/unlink-company?accountId=uuid
|
|
```
|
|
Účel: Odlinkovať firmu od kontaktu
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/contacts/:contactId/create-company?accountId=uuid
|
|
```
|
|
Účel: Vytvoriť firmu z kontaktu
|
|
Body: (optional) { name, email, phone, ... }
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
Efekt: Vytvorí company, nastaví contact.companyId
|
|
```
|
|
|
|
---
|
|
|
|
### 📧 EMAIL ACCOUNTS
|
|
|
|
#### GET /api/email-accounts
|
|
```
|
|
Účel: Zoznam mojich email účtov
|
|
Auth: Áno
|
|
Response: Array of accounts (bez passwords!)
|
|
```
|
|
|
|
#### GET /api/email-accounts/:id
|
|
```
|
|
Účel: Detail email accountu
|
|
Auth: Áno
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/email-accounts
|
|
```
|
|
Účel: Pripojiť email account
|
|
Body: { email*, emailPassword* }
|
|
Auth: Áno
|
|
Rate Limit: Áno
|
|
Efekt:
|
|
- Validate credentials (JMAP)
|
|
- Encrypt password (AES-256-GCM)
|
|
- Ak existuje → share (shared=true)
|
|
- Ak prvý → set primary
|
|
- Sync contacts
|
|
Volá: email.service, password.encryptPassword()
|
|
```
|
|
|
|
#### PATCH /api/email-accounts/:id/password
|
|
```
|
|
Účel: Zmeniť heslo k emailu
|
|
Body: { emailPassword* }
|
|
Auth: Áno
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### PATCH /api/email-accounts/:id/status
|
|
```
|
|
Účel: Aktivovať/deaktivovať email account
|
|
Body: { isActive* }
|
|
Auth: Áno
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/email-accounts/:id/set-primary
|
|
```
|
|
Účel: Nastaviť ako primárny email
|
|
Auth: Áno
|
|
Efekt: Ostatné accounts → isPrimary = false
|
|
```
|
|
|
|
#### DELETE /api/email-accounts/:id
|
|
```
|
|
Účel: Odstrániť prístup k emailu
|
|
Auth: Áno
|
|
Rate Limit: Áno
|
|
Efekt:
|
|
- Ak posledný user → delete account + data
|
|
- Ak shared → iba unlink
|
|
```
|
|
|
|
---
|
|
|
|
### ✉️ EMAILS (CRM Email Management)
|
|
|
|
#### GET /api/emails?accountId=uuid
|
|
```
|
|
Účel: Zoznam emailov
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
Response: Iba emaily od pridaných kontaktov!
|
|
```
|
|
|
|
#### GET /api/emails/thread/:threadId?accountId=uuid
|
|
```
|
|
Účel: Email thread (konverzácia)
|
|
Auth: Áno
|
|
Response: Všetky emaily v threade, sorted by date
|
|
```
|
|
|
|
#### GET /api/emails/search?accountId=&query=
|
|
```
|
|
Účel: Search v uložených emailoch (DB)
|
|
Query: accountId, query (min 2 chars)
|
|
Auth: Áno
|
|
Limit: 50 results
|
|
```
|
|
|
|
#### GET /api/emails/search-jmap?accountId=&query=
|
|
```
|
|
Účel: Full-text search cez JMAP server
|
|
Query: accountId, query
|
|
Auth: Áno
|
|
Volá: jmap.service.searchEmailsJMAP()
|
|
Poznámka: Hľadá vo VŠETKÝCH emailoch, nie len v DB
|
|
```
|
|
|
|
#### GET /api/emails/unread-count?accountId=uuid
|
|
```
|
|
Účel: Počet neprečítaných emailov
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
Response: { totalUnread, byAccount: [...] }
|
|
```
|
|
|
|
#### POST /api/emails/sync?accountId=uuid
|
|
```
|
|
Účel: Synchronizovať najnovšie emaily z JMAP
|
|
Query: accountId (required)
|
|
Auth: Áno
|
|
Efekt: Fetch latest emails, store to DB
|
|
Volá: jmap.service.syncEmails()
|
|
```
|
|
|
|
#### POST /api/emails/thread/:threadId/read?accountId=uuid
|
|
```
|
|
Účel: Označiť thread ako prečítaný
|
|
Auth: Áno
|
|
Efekt: UPDATE emails SET isRead = true + sync JMAP
|
|
```
|
|
|
|
#### GET /api/emails/contact/:contactId?accountId=uuid
|
|
```
|
|
Účel: Emaily od konkrétneho kontaktu
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/emails/contact/:contactId/read?accountId=uuid
|
|
```
|
|
Účel: Označiť všetky emaily kontaktu ako prečítané
|
|
Auth: Áno
|
|
```
|
|
|
|
#### PATCH /api/emails/:jmapId/read?accountId=uuid
|
|
```
|
|
Účel: Označiť jeden email ako read/unread
|
|
Body: { isRead* }
|
|
Auth: Áno
|
|
Poznámka: NEVYUŽÍVA SA vo frontende
|
|
```
|
|
|
|
#### POST /api/emails/reply
|
|
```
|
|
Účel: Odpovedať na email / poslať nový
|
|
Body: { to*, subject*, body*, inReplyTo?, threadId? }
|
|
Auth: Áno
|
|
Efekt: Send via JMAP, store to DB
|
|
Volá: jmap.service.sendEmail()
|
|
```
|
|
|
|
---
|
|
|
|
### ⏱️ TIME TRACKING
|
|
|
|
#### POST /api/time-tracking/start
|
|
```
|
|
Účel: Spustiť nový časovač
|
|
Body: { projectId?, todoId?, companyId?, description? }
|
|
Auth: Áno
|
|
Response: { entry with isRunning: true }
|
|
Efekt:
|
|
- Automaticky zastaví predchádzajúci bežiaci časovač (ak existuje)
|
|
- Vytvorí nový time entry s startTime = NOW()
|
|
```
|
|
|
|
#### POST /api/time-tracking/:entryId/stop
|
|
```
|
|
Účel: Zastaviť bežiaci časovač
|
|
Params: entryId (UUID)
|
|
Body: { projectId?, todoId?, companyId?, description? }
|
|
Auth: Áno
|
|
Response: { entry with endTime, duration, isRunning: false }
|
|
Efekt:
|
|
- Nastaví endTime = NOW()
|
|
- Vypočíta duration v minútach
|
|
- Optional: update projektId/todoId/companyId/description
|
|
```
|
|
|
|
#### GET /api/time-tracking/running
|
|
```
|
|
Účel: Získať aktuálny bežiaci časovač
|
|
Auth: Áno
|
|
Response: { entry } alebo null
|
|
```
|
|
|
|
#### GET /api/time-tracking?projectId=&todoId=&companyId=&startDate=&endDate=
|
|
```
|
|
Účel: Zoznam všetkých time entries s filtrami
|
|
Query: projectId, todoId, companyId, startDate, endDate (all optional)
|
|
Auth: Áno
|
|
Response: Array of time entries
|
|
Order: DESC by startTime
|
|
```
|
|
|
|
#### GET /api/time-tracking/month/:year/:month
|
|
```
|
|
Účel: Mesačný prehľad time entries
|
|
Params: year (YYYY), month (1-12)
|
|
Auth: Áno
|
|
Response: Array of entries pre daný mesiac
|
|
```
|
|
|
|
#### GET /api/time-tracking/stats/monthly/:year/:month
|
|
```
|
|
Účel: Mesačné štatistiky
|
|
Params: year (YYYY), month (1-12)
|
|
Auth: Áno
|
|
Response: {
|
|
totalMinutes,
|
|
totalHours,
|
|
remainingMinutes,
|
|
daysWorked,
|
|
averagePerDay,
|
|
entriesCount,
|
|
byProject: { projectId: minutes },
|
|
byCompany: { companyId: minutes }
|
|
}
|
|
```
|
|
|
|
#### GET /api/time-tracking/:entryId
|
|
```
|
|
Účel: Detail time entry
|
|
Params: entryId (UUID)
|
|
Auth: Áno
|
|
Response: Single time entry
|
|
```
|
|
|
|
#### GET /api/time-tracking/:entryId/details
|
|
```
|
|
Účel: Detail time entry s reláciami
|
|
Params: entryId (UUID)
|
|
Auth: Áno
|
|
Response: { ...entry, project, todo, company }
|
|
```
|
|
|
|
#### PATCH /api/time-tracking/:entryId
|
|
```
|
|
Účel: Upraviť time entry
|
|
Params: entryId (UUID)
|
|
Body: { startTime?, endTime?, projectId?, todoId?, companyId?, description? }
|
|
Auth: Áno
|
|
Validation:
|
|
- Entry nesmie byť isRunning (nemožno upraviť bežiaci časovač)
|
|
- User musí byť owner
|
|
- Pri zmene času sa prepočíta duration
|
|
Response: Updated entry with isEdited: true
|
|
```
|
|
|
|
#### DELETE /api/time-tracking/:entryId
|
|
```
|
|
Účel: Zmazať time entry
|
|
Params: entryId (UUID)
|
|
Auth: Áno
|
|
Validation:
|
|
- Entry nesmie byť isRunning
|
|
- User musí byť owner
|
|
```
|
|
|
|
---
|
|
|
|
### 📊 TIMESHEETS
|
|
|
|
#### POST /api/timesheets/upload
|
|
```
|
|
Účel: Upload timesheet file
|
|
Content-Type: multipart/form-data
|
|
Form: file (PDF/Excel), year (YYYY), month (1-12)
|
|
Auth: Áno
|
|
Validation: Max 10MB
|
|
```
|
|
|
|
#### GET /api/timesheets/my
|
|
```
|
|
Účel: Moje timesheets
|
|
Auth: Áno
|
|
```
|
|
|
|
#### GET /api/timesheets/all
|
|
```
|
|
Účel: Všetky timesheets (admin)
|
|
Auth: Áno (admin only)
|
|
```
|
|
|
|
#### GET /api/timesheets/:timesheetId/download
|
|
```
|
|
Účel: Stiahnuť timesheet file
|
|
Auth: Áno
|
|
Response: File download
|
|
```
|
|
|
|
#### DELETE /api/timesheets/:timesheetId
|
|
```
|
|
Účel: Zmazať timesheet
|
|
Auth: Áno
|
|
Efekt: Delete file + DB record
|
|
```
|
|
|
|
---
|
|
|
|
## VZŤAHY MEDZI SLUŽBAMI
|
|
|
|
### Call Graph (kto volá koho)
|
|
|
|
```
|
|
AUTH FLOW:
|
|
auth.controller
|
|
→ auth.service
|
|
→ emailAccountService.createEmailAccount()
|
|
→ email.service.validateJmapCredentials()
|
|
→ password.encryptPassword()
|
|
→ contact.service.syncContactsForAccount()
|
|
→ jmap.service.discoverContactsFromJMAP()
|
|
→ password.hashPassword()
|
|
→ jwt.generateTokenPair()
|
|
|
|
CONTACT CREATION:
|
|
contact.controller
|
|
→ contact.service.addContact()
|
|
→ jmap.service.syncEmailsFromSender()
|
|
→ jmapRequest() → JMAP Server
|
|
→ INSERT emails to DB
|
|
|
|
COMPANY FROM CONTACT:
|
|
contact.controller
|
|
→ contact.service.createCompanyFromContact()
|
|
→ company.service.createCompany()
|
|
→ UPDATE contact.companyId
|
|
|
|
PROJECT TEAM:
|
|
project.controller
|
|
→ project.service.assignUserToProject()
|
|
→ Validate user exists (users table)
|
|
→ INSERT projectUsers
|
|
|
|
EMAIL SEND:
|
|
email.controller
|
|
→ jmap.service.sendEmail()
|
|
→ jmapRequest() → JMAP Server
|
|
→ INSERT email to DB (sent)
|
|
|
|
TIME TRACKING:
|
|
time-tracking.controller
|
|
→ time-tracking.service.startTimeEntry()
|
|
→ Check running entry (auto-stop if exists)
|
|
→ Validate project/todo/company
|
|
→ INSERT timeEntries
|
|
→ time-tracking.service.stopTimeEntry()
|
|
→ Calculate duration
|
|
→ UPDATE timeEntries (set endTime, isRunning = false)
|
|
→ time-tracking.service.getMonthlyStats()
|
|
→ Aggregate statistics by project/company
|
|
|
|
AUDIT:
|
|
Rôzne controllers
|
|
→ audit.service.logAuditEvent()
|
|
→ INSERT auditLogs
|
|
```
|
|
|
|
### Service Dependencies
|
|
|
|
**Tier 1 (No dependencies):**
|
|
- `password.js` (util)
|
|
- `jwt.js` (util)
|
|
- `logger.js` (util)
|
|
- `errors.js` (util)
|
|
|
|
**Tier 2 (Only utils):**
|
|
- `email.service` → (iba HTTP call)
|
|
- `audit.service` → (iba DB)
|
|
|
|
**Tier 3 (Utils + Tier 2):**
|
|
- `jmap.service` → database, HTTP
|
|
- `company.service` → database, errors
|
|
- `todo.service` → database, errors
|
|
- `note.service` → database, errors
|
|
- `time-tracking.service` → database, errors
|
|
|
|
**Tier 4 (Multiple services):**
|
|
- `contact.service` → company.service, jmap.service
|
|
- `emailAccountService` → email.service, password, contact.service
|
|
- `project.service` → database, errors
|
|
|
|
**Tier 5 (Highest level):**
|
|
- `auth.service` → emailAccountService, password, jwt
|
|
|
|
---
|
|
|
|
## POZNÁMKY
|
|
|
|
### Bezpečnosť
|
|
- **User passwords:** bcrypt (12 rounds, one-way hash)
|
|
- **Email passwords:** AES-256-GCM (reversible, pre JMAP login)
|
|
- **JWT tokens:** HS256, httpOnly cookies
|
|
- **Rate limiting:** Login, password change, email operations
|
|
|
|
### Performance
|
|
- **Database indexy:** Na všetkých foreign keys
|
|
- **Eager loading:** `getWithRelations()` metódy použiť iba ak treba všetko
|
|
- **Pagination:** Momentálne nie je, odporúčam pridať na lists
|
|
|
|
### Maintenance
|
|
- **Error handling:** Centralizované cez `formatErrorResponse()`
|
|
- **Logging:** Štruktúrované cez `logger`
|
|
- **Audit trail:** Všetky kritické akcie logované
|
|
|
|
### Možné zlepšenia
|
|
1. Pagination na všetkých list endpointoch
|
|
2. WebSocket pre real-time notifications (time tracking updates, email sync)
|
|
3. Background jobs pre email sync (Bull/Redis)
|
|
4. Cache layer (Redis) pre často čítané dáta
|
|
5. API versioning (/api/v1/)
|
|
6. GraphQL ako alternatíva k REST
|
|
7. Implement standalone Notes UI alebo vymazať routes
|
|
8. Time tracking export do CSV/Excel pre reporting
|
|
9. Team time tracking dashboard (admin view všetkých userov)
|
|
|
|
---
|
|
|
|
## ⚠️ TECHNICKÉ POZNÁMKY A GOTCHAS
|
|
|
|
### 1. Drizzle ORM - Join Query Pattern
|
|
|
|
**PROBLÉM:** Drizzle ORM nepodporuje nested object syntax v `.select()`
|
|
|
|
```javascript
|
|
// ❌ NEFUNGUJE - TypeError: Cannot convert undefined or null to object
|
|
const wrong = await db
|
|
.select({
|
|
id: projectUsers.id,
|
|
user: {
|
|
id: users.id,
|
|
username: users.username,
|
|
}
|
|
})
|
|
.from(projectUsers)
|
|
.leftJoin(users, eq(projectUsers.userId, users.id));
|
|
|
|
// ✅ SPRÁVNE RIEŠENIE
|
|
const rawResults = await db
|
|
.select() // Bez parametrov!
|
|
.from(projectUsers)
|
|
.leftJoin(users, eq(projectUsers.userId, users.id))
|
|
.where(eq(projectUsers.projectId, projectId))
|
|
.orderBy(desc(projectUsers.addedAt));
|
|
|
|
// Následne mapovať výsledky:
|
|
const assignedUsers = rawResults.map((row) => ({
|
|
id: row.project_users.id, // snake_case table name
|
|
userId: row.project_users.userId,
|
|
role: row.project_users.role,
|
|
user: row.users ? { // null check pre LEFT JOIN
|
|
id: row.users.id,
|
|
username: row.users.username,
|
|
email: row.users.email,
|
|
} : null,
|
|
}));
|
|
```
|
|
|
|
**Kde sa to používa:**
|
|
- `project.service.js`: `getProjectUsers()`, `assignUserToProject()`, `updateUserRoleOnProject()`, `getProjectWithRelations()`
|
|
- Všade kde robíme LEFT JOIN s users, companies, projects, atď.
|
|
|
|
---
|
|
|
|
### 2. Todo Status - Enum vs Boolean
|
|
|
|
**PROBLÉM:** Frontend pôvodne checkoval `todo.completed` (boolean), ale databáza má `status` enum.
|
|
|
|
**Databázová schéma:**
|
|
```javascript
|
|
status: pgEnum('todo_status', ['pending', 'in_progress', 'completed', 'cancelled'])
|
|
```
|
|
|
|
**SPRÁVNE použitie vo frontende:**
|
|
```javascript
|
|
// ✅ SPRÁVNE
|
|
const isCompleted = todo.status === 'completed';
|
|
|
|
// ❌ NESPRÁVNE - toto pole neexistuje!
|
|
const isCompleted = todo.completed;
|
|
```
|
|
|
|
**Toggle endpoint:**
|
|
```javascript
|
|
// Backend: /api/todos/:todoId/toggle
|
|
// Toggleuje medzi: 'completed' ↔ 'pending'
|
|
const newStatus = todo.status === 'completed' ? 'pending' : 'completed';
|
|
```
|
|
|
|
**Kde sa to používa:**
|
|
- Frontend: `TodoItem.jsx`, `TodosPage.jsx`, `ProjectNotesModal.jsx`
|
|
- Backend: `todo.service.js` - `toggleTodoComplete()`
|
|
|
|
---
|
|
|
|
### 3. Node.js Module Caching
|
|
|
|
**PROBLÉM:** Po zmene kódu server niekedy beží stále starý kód, aj keď nodemon reštartoval.
|
|
|
|
**PRÍČINY:**
|
|
- Viacero nodemon procesov naraz (v separátnych termináloch)
|
|
- ESM module caching
|
|
- Neukončené background procesy
|
|
|
|
**RIEŠENIE:**
|
|
```bash
|
|
# Zabij všetky node/nodemon procesy
|
|
ps aux | grep -E "node|nodemon" | grep -v grep | awk '{print $2}' | xargs kill -9
|
|
|
|
# Alebo len na porte 5000
|
|
lsof -i:5000 | grep LISTEN | awk '{print $2}' | xargs kill -9
|
|
|
|
# Clear cache a reštart
|
|
rm -rf node_modules/.cache
|
|
npm run dev
|
|
```
|
|
|
|
**PREVENCIA:**
|
|
- Nespúšťať server v background mode počas developmentu
|
|
- Používať iba jeden terminál pre server
|
|
- Check procesy: `ps aux | grep node`
|
|
|
|
---
|
|
|
|
### 4. Frontend API Response Parsing
|
|
|
|
**PROBLÉM:** API môže vracať rôzne formáty odpovede.
|
|
|
|
**Admin API - Get All Users:**
|
|
```javascript
|
|
// Backend vracia:
|
|
{ users: [...], count: 10 }
|
|
|
|
// Frontend musí extrahovať:
|
|
const usersList = Array.isArray(data) ? data : (data?.users || [])
|
|
```
|
|
|
|
**Project API - Get Team Members:**
|
|
```javascript
|
|
// Backend vracia priamo array:
|
|
[{ id, userId, user: {...} }]
|
|
|
|
// Frontend očakáva array:
|
|
setTeamMembers(data || [])
|
|
```
|
|
|
|
**BEST PRACTICE:**
|
|
- Vždy checkuj `Array.isArray()`
|
|
- Fallback na prázdne array: `data || []`
|
|
- Console.log response v dev mode pre debug
|
|
|
|
---
|
|
|
|
### 5. Database Constraints
|
|
|
|
**projectUsers table - UNIQUE constraint:**
|
|
```javascript
|
|
unique('project_user_unique').on(table.projectId, table.userId)
|
|
```
|
|
|
|
**Význam:**
|
|
- User môže byť assigned k projektu **iba raz**
|
|
- Duplicate assignment → `409 ConflictError`
|
|
|
|
**Cascade deletes:**
|
|
```javascript
|
|
onDelete: 'cascade' // Ak sa zmaže project/user → zmaže sa assignment
|
|
```
|
|
|
|
**Kde sa používa:**
|
|
- Projects: CASCADE delete na todos, notes, timesheets, projectUsers
|
|
- Companies: CASCADE delete na projects (a všetko pod nimi)
|
|
- Email accounts: CASCADE delete na contacts, emails
|
|
|
|
---
|
|
|
|
### 6. Authentication Middleware
|
|
|
|
**Každá route (okrem /api/auth/login) vyžaduje authentication:**
|
|
|
|
```javascript
|
|
router.use(authenticate); // Global middleware na route file
|
|
```
|
|
|
|
**JWT token sa číta z:**
|
|
1. httpOnly cookie `accessToken` (preferované)
|
|
2. Authorization header `Bearer <token>` (fallback)
|
|
|
|
**User info v request:**
|
|
```javascript
|
|
req.userId // UUID
|
|
req.username // String
|
|
req.userRole // 'admin' | 'member'
|
|
```
|
|
|
|
---
|
|
|
|
### 7. Time Tracking - Auto-stop Behavior
|
|
|
|
**PROBLÉM:** User môže mať spustený iba jeden časovač naraz.
|
|
|
|
**RIEŠENIE - Automatický stop:**
|
|
```javascript
|
|
// Pri štarte nového časovača
|
|
if (existingRunning) {
|
|
// Automaticky zastaví predchádzajúci časovač
|
|
const endTime = new Date();
|
|
const duration = Math.round((endTime - startTime) / 60000); // minúty
|
|
await db.update(timeEntries)
|
|
.set({ endTime, duration, isRunning: false })
|
|
.where(eq(timeEntries.id, existingRunning.id));
|
|
}
|
|
```
|
|
|
|
**Validačné pravidlá:**
|
|
```javascript
|
|
// ✅ POVOLENÉ
|
|
- Spustiť nový časovač (auto-stop predchádzajúceho)
|
|
- Upraviť zastavený časovač
|
|
- Zmazať zastavený časovač
|
|
|
|
// ❌ ZAKÁZANÉ
|
|
- Upraviť bežiaci časovač (musí sa najprv zastaviť)
|
|
- Zmazať bežiaci časovač (musí sa najprv zastaviť)
|
|
- Zastaviť časovač iného usera
|
|
```
|
|
|
|
**Duration calculation:**
|
|
- Ukladá sa v **minútach** (Math.round)
|
|
- Počíta sa z rozdelu: `(endTime - startTime) / 60000`
|
|
- Frontend zobrazuje: hodiny + minúty (napr. "2h 35m")
|
|
|
|
**isEdited flag:**
|
|
- Automaticky nastavený pri manuálnej úprave
|
|
- Indikuje, že čas bol zmenený používateľom (nie auto-tracked)
|
|
|
|
**Kde sa používa:**
|
|
- `time-tracking.service.js`: `startTimeEntry()`, `updateTimeEntry()`
|
|
- Frontend: Time tracking komponenty s real-time countdown
|
|
|
|
---
|
|
|
|
## 🔍 DEBUGGING TIPS
|
|
|
|
### 1. Server beží starý kód
|
|
```bash
|
|
# Check running processes
|
|
ps aux | grep node
|
|
|
|
# Kill all and restart
|
|
pkill -9 node && npm run dev
|
|
```
|
|
|
|
### 2. Database query debugging
|
|
```javascript
|
|
// Drizzle query debugging
|
|
const result = await db.select()...
|
|
console.log('[DEBUG] Raw DB result:', JSON.stringify(result, null, 2));
|
|
```
|
|
|
|
### 3. Frontend API debugging
|
|
```javascript
|
|
// V API function
|
|
console.log('[DEBUG] API Response:', data);
|
|
console.log('[DEBUG] Extracted users:', usersList);
|
|
```
|
|
|
|
### 4. JMAP email issues
|
|
```javascript
|
|
// Check JMAP credentials
|
|
const valid = await validateJmapCredentials(email, password);
|
|
console.log('[DEBUG] JMAP validation:', valid);
|
|
```
|
|
|
|
---
|
|
|
|
**Vytvorené:** 2025-11-21
|
|
**Posledná aktualizácia:** 2025-11-24
|
|
**Autor:** CRM Server Team
|
|
**Kontakt:** crm-server documentation
|