Files
crm-server/MIGRATION_GUIDE.md
richardtekula bb851639b8 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>
2025-11-21 08:35:30 +01:00

12 KiB

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

# 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:

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:

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

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

# 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:

    # 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.

# 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:

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:

// 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:

// 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:

# 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:

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:

# 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ť):

// users tabuľka obsahovala email credentials
const user = await db.select().from(users).where(eq(users.email, email));

Nové (použiť):

// 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é:

await db.insert(users).values({
  username,
  email,
  emailPassword,
  jmapAccountId,
});

Nové:

// 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