Add Timesheets API with file upload and role-based access

Backend Features:
- Timesheets database table (id, userId, fileName, filePath, fileType, fileSize, year, month, timestamps)
- File upload with multer (memory storage, 10MB limit, PDF/Excel validation)
- Structured file storage: uploads/timesheets/{userId}/{year}/{month}/
- RESTful API endpoints:
  * POST /api/timesheets/upload - Upload timesheet
  * GET /api/timesheets/my - Get user's timesheets (with filters)
  * GET /api/timesheets/all - Get all timesheets (admin only)
  * GET /api/timesheets/:id/download - Download file
  * DELETE /api/timesheets/:id - Delete timesheet
- Role-based permissions: users access own files, admins access all
- Proper error handling and file cleanup on errors
- Database migration for timesheets table

Technical:
- Uses req.user.role for permission checks
- Automatic directory creation for user/year/month structure
- Blob URL cleanup and proper file handling
- Integration with existing auth middleware

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-11-21 08:35:30 +01:00
parent 05be898259
commit bb851639b8
27 changed files with 2847 additions and 532 deletions

445
MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,445 @@
# Migračný návod - Refaktorizácia emailových účtov
## Prehľad zmien
Tento refaktoring rieši nasledujúce problémy:
### 🐛 Opravené bugy:
1. **Bug s vytváraním používateľov** - Email credentials sa teraz ukladajú do `emailAccounts` tabuľky namiesto `users` tabuľky
2. **Bug s duplikátnymi kontaktami** - Pridaný composite unique constraint zabezpečuje správnu izoláciu kontaktov
3. **Bug s linkovaním emailov** - `linkEmail` funkcia teraz používa `emailAccounts` tabuľku
4. **Nepoužívané stĺpce** - Odstránené `email`, `email_password`, `jmap_account_id` z `users` tabuľky
### ✨ Vylepšenia:
- Čistejšia separácia concerns (users vs email accounts)
- Podpora pre viacero emailových účtov na používateľa
- Pripravené na zdieľané emailové účty (many-to-many)
---
## 📋 Migračný proces
### Krok 1: Backup databázy
```bash
# PostgreSQL backup
pg_dump -U your_username -d your_database > backup_$(date +%Y%m%d).sql
```
### Krok 2: Migrovať existujúce email credentials
Tento script presunie email credentials z `users` tabuľky do `emailAccounts` tabuľky:
```bash
cd /home/richardtekula/Documents/WORK/crm-server
node src/scripts/migrate-users-to-email-accounts.js
```
**Čo tento script robí:**
- Nájde všetkých používateľov s `email`, `emailPassword`, `jmapAccountId`
- Vytvorí zodpovedajúce záznamy v `emailAccounts` tabuľke
- Nastaví prvý účet ako primárny (`isPrimary = true`)
- Zachová originálne dáta v `users` tabuľke (pre bezpečnosť)
**Výstup:**
```
🔄 Starting migration from users table to emailAccounts table...
Found 5 users with email credentials
📧 Processing user: admin (admin@example.com)
✅ Created email account: admin@example.com
- Account ID: abc-123-def
- Is Primary: true
✅ Migration complete!
- Migrated: 5 accounts
- Skipped: 0 accounts (already exist)
```
### Krok 3: Opraviť duplikátne kontakty
Ak máte duplikátne kontakty (ten istý email viackrát pre jedného usera), spustite:
```bash
node src/scripts/fix-duplicate-contacts.js
```
**Čo tento script robí:**
- Nájde duplikátne kontakty (rovnaký `userId` + `email`)
- Zachová najnovší kontakt
- Premapuje všetky emaily na najnovší kontakt
- Vymaže staré duplikáty
### Krok 4: Pridať unique constraint na contacts
```bash
node src/scripts/add-contacts-unique-constraint.js
```
**Čo tento script robí:**
- Pridá composite unique constraint: `(user_id, email_account_id, email)`
- Zabráni vzniku nových duplikátov
**Poznámka:** Ak tento krok zlyhá kvôli existujúcim duplikátom, najprv spustite Krok 3.
### Krok 5: Aktualizovať kód aplikácie
Kód je už aktualizovaný v týchto súboroch:
**Backend:**
-`src/db/schema.js` - odstránené staré stĺpce z users tabuľky
-`src/controllers/admin.controller.js` - `createUser` používa emailAccounts
-`src/services/auth.service.js` - `linkEmail` používa emailAccounts
-`src/services/auth.service.js` - `getUserById` vracia emailAccounts
-`src/controllers/admin.controller.js` - `getAllUsers` nevracia staré stĺpce
**Žiadne zmeny nie sú potrebné na frontende** - API rozhranie zostáva kompatibilné.
### Krok 6: Reštartovať aplikáciu
```bash
# Zastaviť aplikáciu
pm2 stop crm-server
# Reštartovať aplikáciu
pm2 start crm-server
# Alebo pomocou npm
npm run dev
```
### Krok 7: Overiť funkčnosť
1. **Prihlásenie:**
- Skúste sa prihlásiť s existujúcim používateľom
- Overte že vidíte svoje email účty v sidebari
2. **Vytvorenie nového používateľa:**
```bash
# Admin vytvorí nového používateľa s emailom
# V Profile -> Create User -> vyplniť email + heslo
```
- Overte že email účet sa objavil v `emailAccounts` tabuľke
- Overte že používateľ sa môže prihlásiť a vidí svoj email účet
3. **Pridanie kontaktu:**
- Prejdite do Inbox
- Discover kontakty
- Pridajte nový kontakt
- Overte že kontakt sa zobrazuje len pre daný email účet
4. **Označenie emailov ako prečítané:**
- Otvorte Email Conversations
- Kliknite na "Mark all as read" button na kontakte
- Overte že všetky emaily od daného kontaktu sú označené ako prečítané
### Krok 8: Vyčistiť staré dáta (VOLITEĽNÉ)
**⚠️ POZOR:** Tento krok je nevratný! Spustite ho len po dôkladnom overení funkčnosti.
```bash
# Odstráni staré stĺpce z users tabuľky
node src/scripts/remove-old-user-email-columns.js
```
Tento script odstráni:
- `users.email`
- `users.email_password`
- `users.jmap_account_id`
**Po spustení tohto scriptu nie je možné sa vrátiť k starej verzii kódu!**
---
## 🔮 Budúce vylepšenia: Zdieľané emailové účty
### Súčasný stav:
- Jeden emailový účet patrí jednému používateľovi
- `emailAccounts.userId` je foreign key
### Plánovaný stav (many-to-many):
- Jeden emailový účet môže byť zdieľaný medzi viacerými používateľmi
- Dvaja admini môžu mať linknutý ten istý email account
- Keď jeden odpíše, druhý to vidí
### Návrh implementácie:
#### 1. Nová junction table:
```sql
CREATE TABLE user_email_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email_account_id UUID NOT NULL REFERENCES email_accounts(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member', -- 'owner' | 'member'
can_send BOOLEAN NOT NULL DEFAULT true,
can_read BOOLEAN NOT NULL DEFAULT true,
added_at TIMESTAMP NOT NULL DEFAULT NOW(),
added_by UUID REFERENCES users(id),
UNIQUE(user_id, email_account_id)
);
```
#### 2. Zmeny v existujúcich tabuľkách:
**emailAccounts:**
- Odstráni `userId` foreign key
- Pridá `ownerId` (prvý používateľ, ktorý vytvoril účet)
- Pridá `shared` boolean flag
**contacts:**
- Zostáva `emailAccountId` (kontakty patria k email účtu, nie k používateľovi)
- Odstráni `userId` (alebo ho ponechá ako "created_by")
**emails:**
- Zostáva `emailAccountId`
- Pridá `sent_by_user_id` (kto poslal odpoveď)
#### 3. Migračný script:
```javascript
// Presunie existujúce väzby do user_email_accounts
async function migrateToSharedAccounts() {
const accounts = await db.select().from(emailAccounts);
for (const account of accounts) {
await db.insert(userEmailAccounts).values({
userId: account.userId,
emailAccountId: account.id,
role: 'owner',
canSend: true,
canRead: true,
});
}
// Alter table to remove userId, add ownerId
await db.execute(sql`
ALTER TABLE email_accounts
DROP COLUMN user_id,
ADD COLUMN owner_id UUID REFERENCES users(id),
ADD COLUMN shared BOOLEAN DEFAULT false
`);
}
```
#### 4. API zmeny:
**Nové endpointy:**
```javascript
// Zdieľať email account s iným používateľom
POST /api/email-accounts/:accountId/share
Body: { userId, role: 'member', canSend: true, canRead: true }
// Odobrať prístup
DELETE /api/email-accounts/:accountId/share/:userId
// Získať používateľov s prístupom k účtu
GET /api/email-accounts/:accountId/users
```
#### 5. Frontend zmeny:
- Zobrazenie ikony "zdieľané" pri zdieľaných účtoch
- UI pre zdieľanie účtov (modal s výberom používateľov)
- Zobrazenie "sent by User X" pri odpovediach
---
## 🐛 Riešenie problémov
### Problém: Script fail kvôli duplikátom
**Chyba:**
```
could not create unique index "user_account_email_unique"
Key (user_id, email_account_id, email)=(...) already exists.
```
**Riešenie:**
```bash
# Najprv opravte duplikáty
node src/scripts/fix-duplicate-contacts.js
# Potom pridajte constraint
node src/scripts/add-contacts-unique-constraint.js
```
### Problém: Používateľ nevidí email účty po migrácii
**Kontrola:**
```sql
SELECT * FROM email_accounts WHERE user_id = 'user-id-here';
```
**Riešenie:**
- Skontrolujte či migračný script úspešne bežal
- Skontrolujte logy servera
- Reštartujte server
### Problém: "Mark as read" button nefunguje
**Možné príčiny:**
1. Backend endpoint `/emails/contact/:contactId/read` neexistuje
2. `contactId` je null alebo nesprávne
3. Email account nie je správne linknutý
**Kontrola:**
```bash
# Skontrolujte logs
tail -f logs/server.log
# Test endpoint
curl -X POST http://localhost:3000/api/emails/contact/<contactId>/read \
-H "Authorization: Bearer <token>"
```
---
## 📊 Databázová schéma (po migrácii)
### users
```
id | UUID | PRIMARY KEY
username | TEXT | UNIQUE NOT NULL
first_name | TEXT |
last_name | TEXT |
password | TEXT | bcrypt hash
temp_password | TEXT | bcrypt hash
changed_password| BOOLEAN | DEFAULT false
role | ENUM | 'admin' | 'member'
last_login | TIMESTAMP |
created_at | TIMESTAMP |
updated_at | TIMESTAMP |
```
### email_accounts
```
id | UUID | PRIMARY KEY
user_id | UUID | REFERENCES users(id) CASCADE
email | TEXT | NOT NULL
email_password | TEXT | NOT NULL (encrypted)
jmap_account_id | TEXT | NOT NULL
is_primary | BOOLEAN | DEFAULT false
is_active | BOOLEAN | DEFAULT true
created_at | TIMESTAMP |
updated_at | TIMESTAMP |
```
### contacts
```
id | UUID | PRIMARY KEY
user_id | UUID | REFERENCES users(id) CASCADE
email_account_id| UUID | REFERENCES email_accounts(id) CASCADE
email | TEXT | NOT NULL
name | TEXT |
notes | TEXT |
added_at | TIMESTAMP |
created_at | TIMESTAMP |
updated_at | TIMESTAMP |
UNIQUE (user_id, email_account_id, email)
```
### emails
```
id | UUID | PRIMARY KEY
user_id | UUID | REFERENCES users(id) CASCADE
email_account_id| UUID | REFERENCES email_accounts(id) CASCADE
contact_id | UUID | REFERENCES contacts(id) CASCADE
jmap_id | TEXT | UNIQUE
message_id | TEXT | UNIQUE
thread_id | TEXT |
in_reply_to | TEXT |
from | TEXT |
to | TEXT |
subject | TEXT |
body | TEXT |
is_read | BOOLEAN | DEFAULT false
date | TIMESTAMP |
created_at | TIMESTAMP |
updated_at | TIMESTAMP |
```
---
## ✅ Checklist
- [ ] Backup databázy vytvorený
- [ ] Migračný script 1: `migrate-users-to-email-accounts.js` spustený
- [ ] Migračný script 2: `fix-duplicate-contacts.js` spustený
- [ ] Migračný script 3: `add-contacts-unique-constraint.js` spustený
- [ ] Backend kód aktualizovaný (už hotové)
- [ ] Aplikácia reštartovaná
- [ ] Funkčnosť otestovaná:
- [ ] Prihlásenie existujúceho používateľa
- [ ] Vytvorenie nového používateľa s emailom
- [ ] Pridanie nového email účtu
- [ ] Pridanie kontaktu
- [ ] Označenie emailov ako prečítané
- [ ] Odpoveď na email
- [ ] (Voliteľné) Vyčistenie starých stĺpcov: `remove-old-user-email-columns.js` spustené
---
## 📞 Podpora
Ak narazíte na problémy:
1. Skontrolujte logs: `tail -f logs/server.log`
2. Skontrolujte databázu: `psql -U username -d database`
3. Obnovte backup ak je potrebné
---
## 📝 Poznámky pre vývojárov
### Dôležité zmeny v API:
**❌ Staré (nepoužívať):**
```javascript
// users tabuľka obsahovala email credentials
const user = await db.select().from(users).where(eq(users.email, email));
```
**✅ Nové (použiť):**
```javascript
// email credentials sú v emailAccounts tabuľke
const accounts = await db.select()
.from(emailAccounts)
.where(eq(emailAccounts.userId, userId));
```
### Pri vytváraní nového používateľa:
**❌ Staré:**
```javascript
await db.insert(users).values({
username,
email,
emailPassword,
jmapAccountId,
});
```
**✅ Nové:**
```javascript
// 1. Vytvor usera
const [user] = await db.insert(users).values({ username }).returning();
// 2. Vytvor email account
await db.insert(emailAccounts).values({
userId: user.id,
email,
emailPassword,
jmapAccountId,
isPrimary: true,
});
```
---
Vytvoril: Claude Code
Dátum: 2025-11-20
Verzia: 1.0