added backup and email client
This commit is contained in:
@@ -292,76 +292,216 @@ fetch('/api/developer/setup', { method: 'POST' }).then(r => r.json()).then(conso
|
|||||||
# Danach ausloggen und neu einloggen
|
# Danach ausloggen und neu einloggen
|
||||||
```
|
```
|
||||||
|
|
||||||
## Geplante Features
|
## Vertragstypen
|
||||||
|
|
||||||
### E-Mail-Client Integration
|
### Standard-Vertragstypen
|
||||||
|
|
||||||
Ein integrierter E-Mail-Client pro Kunde mit den folgenden Funktionen:
|
Folgende Vertragstypen werden bei Installation/Factory-Reset automatisch angelegt:
|
||||||
|
|
||||||
#### Konzept
|
| Code | Name | Icon | Farbe |
|
||||||
|
|------|------|------|-------|
|
||||||
|
| ELECTRICITY | Strom | Zap | #FFC107 |
|
||||||
|
| GAS | Gas | Flame | #FF5722 |
|
||||||
|
| DSL | DSL | Wifi | #2196F3 |
|
||||||
|
| FIBER | Glasfaser | Cable | #9C27B0 |
|
||||||
|
| CABLE | Kabel Internet (Coax) | Cable | #00BCD4 |
|
||||||
|
| MOBILE | Mobilfunk | Smartphone | #4CAF50 |
|
||||||
|
| TV | TV | Tv | #E91E63 |
|
||||||
|
| CAR_INSURANCE | KFZ-Versicherung | Car | #607D8B |
|
||||||
|
|
||||||
- **E-Mail-Tab in Kundenansicht**: Zeigt alle E-Mails des Kunden via IMAP an
|
> **Hinweis:** Vertragstypen können nur von Benutzern mit **Entwicklerzugriff** geändert werden, da Änderungen auch Anpassungen an den Formularen erfordern.
|
||||||
- **Mehrere E-Mail-Konten**: Dropdown zur Auswahl zwischen verschiedenen `@stressfrei-wechseln.de` Adressen des Kunden
|
|
||||||
- **E-Mails schreiben**: Neue E-Mails verfassen und Antworten auf bestehende E-Mails
|
|
||||||
- **Versand vom ausgewählten Konto**: SMTP-Versand über die gewählte Stressfrei-Wechseln Adresse
|
|
||||||
- **Zuordnung zu Verträgen**: Manuelles Zuordnen von E-Mails zu Verträgen
|
|
||||||
- **E-Mail-Tab in Vertragsansicht**: Zeigt nur dem Vertrag zugeordnete E-Mails (Postfach-Ansicht), mit Antworten/Schreiben-Funktion
|
|
||||||
|
|
||||||
#### Technische Umsetzung
|
### Vertragstyp-spezifische Felder
|
||||||
|
|
||||||
1. **IMAP/SMTP-Zugangsdaten speichern**
|
Je nach Vertragstyp werden unterschiedliche Felder im Formular angezeigt:
|
||||||
- Beim Erstellen einer StressfreiEmail werden die Zugangsdaten vom Email-Provider (Plesk) zurückgegeben
|
|
||||||
- Diese werden verschlüsselt in der Datenbank gespeichert (wie bereits bei Portal-Zugangsdaten)
|
|
||||||
- StressfreiEmail-Tabelle erweitern um: `imapServer`, `imapPort`, `smtpServer`, `smtpPort`, `emailPassword` (verschlüsselt)
|
|
||||||
|
|
||||||
2. **Backend: IMAP-Service**
|
#### Strom & Gas (ELECTRICITY, GAS)
|
||||||
- IMAP-Client zum Abrufen von E-Mails (z.B. `imap-simple` oder `imapflow`)
|
|
||||||
- Endpunkte:
|
|
||||||
- `GET /api/customers/:id/emails` - E-Mails für Kunde abrufen
|
|
||||||
- `GET /api/emails/:id` - Einzelne E-Mail mit Body
|
|
||||||
- `POST /api/emails/send` - E-Mail senden (SMTP)
|
|
||||||
- `PUT /api/emails/:id/assign` - E-Mail einem Vertrag zuordnen
|
|
||||||
|
|
||||||
3. **Frontend: E-Mail-Tab (Kundenansicht)**
|
- Zähler-Auswahl
|
||||||
- Neuer Tab in CustomerDetail: "E-Mails"
|
- Jahresverbrauch (kWh/m³)
|
||||||
- Dropdown oben: Auswahl der StressfreiEmail-Adresse
|
- Grundpreis, Arbeitspreis
|
||||||
- Liste der E-Mails (Betreff, Absender, Datum)
|
- Bonus
|
||||||
- Detail-Ansicht beim Klick
|
- Vorversorger, Kundennummer beim Vorversorger
|
||||||
- "Neue E-Mail" und "Antworten" Buttons
|
|
||||||
- "Vertrag zuordnen" Dropdown in E-Mail-Detail
|
|
||||||
|
|
||||||
4. **Frontend: E-Mail-Tab (Vertragsansicht)**
|
#### Internet (DSL, CABLE, FIBER)
|
||||||
- Neuer Tab in ContractDetail: "E-Mails"
|
|
||||||
- Zeigt nur E-Mails die diesem Vertrag zugeordnet sind
|
|
||||||
- Gleiche Funktionalität wie in Kundenansicht:
|
|
||||||
- Liste der zugeordneten E-Mails
|
|
||||||
- Detail-Ansicht beim Klick
|
|
||||||
- "Neue E-Mail" und "Antworten" Buttons
|
|
||||||
- Versand erfolgt über die StressfreiEmail-Adresse des Kunden
|
|
||||||
|
|
||||||
5. **Datenbank**
|
- Download/Upload (Mbit/s)
|
||||||
```prisma
|
- Router Modell, Seriennummer
|
||||||
model CustomerEmail {
|
- Installationsdatum
|
||||||
id Int @id @default(autoincrement())
|
- Benutzername, Passwort
|
||||||
customerId Int
|
- Rufnummern mit SIP-Zugangsdaten
|
||||||
customer Customer @relation(...)
|
|
||||||
stressfreiEmailId Int
|
|
||||||
stressfreiEmail StressfreiEmail @relation(...)
|
|
||||||
contractId Int? // Optionale Zuordnung zu Vertrag
|
|
||||||
contract Contract? @relation(...)
|
|
||||||
messageId String @unique // IMAP Message-ID
|
|
||||||
subject String
|
|
||||||
fromAddress String
|
|
||||||
toAddress String
|
|
||||||
date DateTime
|
|
||||||
isRead Boolean @default(false)
|
|
||||||
folder String // INBOX, Sent, etc.
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Status
|
| Vertragstyp | Zusatzfeld |
|
||||||
- [ ] Noch nicht implementiert - Plan für zukünftige Version
|
|-------------|------------|
|
||||||
|
| **Glasfaser (FIBER)** | Home-ID |
|
||||||
|
|
||||||
|
#### Mobilfunk (MOBILE)
|
||||||
|
|
||||||
|
- Datenvolumen (GB)
|
||||||
|
- Inklusiv-Minuten, Inklusiv-SMS
|
||||||
|
- Gerät-Modell, IMEI
|
||||||
|
- Multisim-Checkbox
|
||||||
|
- SIM-Karten (dynamisch erweiterbar):
|
||||||
|
- Rufnummer, SIM-Kartennummer (ICCID)
|
||||||
|
- PIN, PUK (verschlüsselt)
|
||||||
|
- Multisim-Flag, Hauptkarte-Flag
|
||||||
|
|
||||||
|
> **Hinweis Multisim:** Nicht buchbar bei Klarmobil, Congstar, Otelo. Benötigt Freenet oder vergleichbar.
|
||||||
|
|
||||||
|
#### TV
|
||||||
|
|
||||||
|
- Receiver Modell
|
||||||
|
- Smartcard-Nummer
|
||||||
|
- Paket/Angebot
|
||||||
|
|
||||||
|
#### KFZ-Versicherung (CAR_INSURANCE)
|
||||||
|
|
||||||
|
- Kennzeichen, HSN, TSN, FIN/VIN
|
||||||
|
- Fahrzeugtyp, Erstzulassung
|
||||||
|
- SF-Klasse (Schadenfreiheitsklasse)
|
||||||
|
- Versicherungsart (Haftpflicht/Teilkasko/Vollkasko)
|
||||||
|
- Selbstbeteiligungen (Teilkasko, Vollkasko)
|
||||||
|
- Versicherungsscheinnummer
|
||||||
|
- Vorversicherer
|
||||||
|
|
||||||
|
### Standard-Anbieter
|
||||||
|
|
||||||
|
Folgende Anbieter werden bei Installation/Factory-Reset automatisch angelegt:
|
||||||
|
|
||||||
|
| Anbieter | Portal-URL |
|
||||||
|
|----------|------------|
|
||||||
|
| Vodafone | https://www.vodafone.de/meinvodafone/account/login |
|
||||||
|
| Klarmobil | https://www.klarmobil.de/login |
|
||||||
|
| Otelo | https://www.otelo.de/mein-otelo/login |
|
||||||
|
| Congstar | https://www.congstar.de/login/ |
|
||||||
|
| Telekom | https://www.telekom.de/kundencenter/startseite |
|
||||||
|
| O2 | https://www.o2online.de/ecare/selfcare |
|
||||||
|
| 1&1 | https://control-center.1und1.de/ |
|
||||||
|
|
||||||
|
### Anbieter-spezifische Felder
|
||||||
|
|
||||||
|
Einige Felder werden nur bei bestimmten Anbietern angezeigt:
|
||||||
|
|
||||||
|
| Anbieter | Vertragstyp | Zusatzfeld |
|
||||||
|
|----------|-------------|------------|
|
||||||
|
| **Vodafone** | DSL, Kabel Internet | Aktivierungscode |
|
||||||
|
|
||||||
|
> **Hinweis Multisim:** Bei Klarmobil, Congstar und Otelo ist Multisim **nicht** buchbar. Dafür wird Freenet oder ein vergleichbarer Anbieter benötigt.
|
||||||
|
|
||||||
|
## E-Mail-Client
|
||||||
|
|
||||||
|
Ein vollständig integrierter E-Mail-Client pro Kunde mit IMAP-Empfang und SMTP-Versand.
|
||||||
|
|
||||||
|
### Funktionen
|
||||||
|
|
||||||
|
#### E-Mails lesen & verwalten
|
||||||
|
- **E-Mail-Tab in Kundenansicht** mit Ordnern: Posteingang, Gesendet, Papierkorb
|
||||||
|
- **Mehrere E-Mail-Konten** pro Kunde (Dropdown zur Auswahl)
|
||||||
|
- **E-Mail-Detailansicht** mit HTML/Text-Body, Absender, Empfänger, CC, Datum
|
||||||
|
- **Gelesen/Ungelesen** markieren
|
||||||
|
- **Favoriten** (Stern) für wichtige E-Mails
|
||||||
|
- **Papierkorb** mit Wiederherstellen und endgültigem Löschen
|
||||||
|
|
||||||
|
#### E-Mails schreiben
|
||||||
|
- **Neue E-Mail verfassen** mit An, CC, Betreff, Text
|
||||||
|
- **Antworten** mit zitiertem Originaltext
|
||||||
|
- **Dateianhänge** (max. 10 MB pro Datei, 25 MB gesamt)
|
||||||
|
- **SMTP-Versand** über die gewählte StressfreiEmail-Adresse
|
||||||
|
|
||||||
|
#### Vertragszuordnung
|
||||||
|
- **E-Mails zu Verträgen zuordnen** für bessere Nachverfolgung
|
||||||
|
- **E-Mail-Tab in Vertragsansicht** zeigt nur zugeordnete E-Mails
|
||||||
|
- **Automatische Zuordnung** bei Versand aus Vertragskontext
|
||||||
|
- **Manuelle Zuordnung** über Suchfeld und Vertragsauswahl
|
||||||
|
|
||||||
|
#### Anhänge
|
||||||
|
- **Anhangsliste** in E-Mail-Detail
|
||||||
|
- **Download** einzelner Anhänge
|
||||||
|
- **Inline-Ansicht** (im Browser öffnen)
|
||||||
|
|
||||||
|
### Technische Details
|
||||||
|
|
||||||
|
#### Backend-Services
|
||||||
|
| Service | Beschreibung |
|
||||||
|
|---------|--------------|
|
||||||
|
| `imapService.ts` | IMAP-Client (ImapFlow) für E-Mail-Empfang |
|
||||||
|
| `smtpService.ts` | SMTP-Client (Nodemailer) für E-Mail-Versand |
|
||||||
|
| `cachedEmail.service.ts` | E-Mail-Caching, Synchronisation, Zuordnung |
|
||||||
|
|
||||||
|
#### API-Endpunkte
|
||||||
|
```
|
||||||
|
GET /api/customers/:id/emails # E-Mails für Kunde
|
||||||
|
GET /api/contracts/:id/emails # E-Mails für Vertrag
|
||||||
|
GET /api/emails/:id # Einzelne E-Mail mit Body
|
||||||
|
POST /api/stressfrei-emails/:id/sync # IMAP-Synchronisation
|
||||||
|
POST /api/stressfrei-emails/:id/send # E-Mail senden
|
||||||
|
POST /api/emails/:id/assign # Vertrag zuordnen
|
||||||
|
DELETE /api/emails/:id/assign # Zuordnung aufheben
|
||||||
|
PATCH /api/emails/:id/read # Gelesen/Ungelesen
|
||||||
|
POST /api/emails/:id/star # Favorit umschalten
|
||||||
|
DELETE /api/emails/:id # In Papierkorb
|
||||||
|
POST /api/emails/:id/restore # Aus Papierkorb wiederherstellen
|
||||||
|
DELETE /api/emails/:id/permanent # Endgültig löschen
|
||||||
|
GET /api/emails/:id/attachments/:filename # Anhang herunterladen
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Datenbank-Modell
|
||||||
|
```prisma
|
||||||
|
model CachedEmail {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
stressfreiEmailId Int
|
||||||
|
stressfreiEmail StressfreiEmail @relation(...)
|
||||||
|
folder String // INBOX, SENT
|
||||||
|
messageId String // RFC 5322 Message-ID
|
||||||
|
uid Int // IMAP UID
|
||||||
|
subject String?
|
||||||
|
fromAddress String
|
||||||
|
fromName String?
|
||||||
|
toAddresses String @db.Text // JSON Array
|
||||||
|
ccAddresses String? @db.Text
|
||||||
|
receivedAt DateTime
|
||||||
|
textBody String? @db.LongText
|
||||||
|
htmlBody String? @db.LongText
|
||||||
|
hasAttachments Boolean @default(false)
|
||||||
|
attachmentNames String? @db.Text // JSON Array
|
||||||
|
contractId Int? // Vertragszuordnung
|
||||||
|
assignedAt DateTime?
|
||||||
|
assignedBy Int?
|
||||||
|
isAutoAssigned Boolean @default(false)
|
||||||
|
isRead Boolean @default(false)
|
||||||
|
isStarred Boolean @default(false)
|
||||||
|
isDeleted Boolean @default(false)
|
||||||
|
deletedAt DateTime?
|
||||||
|
|
||||||
|
@@unique([stressfreiEmailId, messageId, folder])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sicherheit
|
||||||
|
- **Passwort-Verschlüsselung**: AES-256-GCM für Mailbox-Passwörter
|
||||||
|
- **Passwort-Reset**: Neues Passwort generieren und beim Provider setzen
|
||||||
|
- **Verschlüsselungsmodi**: SSL, STARTTLS, oder unverschlüsselt
|
||||||
|
- **Selbstsignierte Zertifikate**: Konfigurierbar pro Provider
|
||||||
|
|
||||||
|
#### Berechtigungen
|
||||||
|
| Aktion | Berechtigung |
|
||||||
|
|--------|--------------|
|
||||||
|
| E-Mails lesen | `customers:read` |
|
||||||
|
| E-Mails senden, markieren | `customers:update` |
|
||||||
|
| Vertrag zuordnen | `contracts:update` |
|
||||||
|
| Löschen, Papierkorb | `emails:delete` |
|
||||||
|
|
||||||
|
### Frontend-Komponenten
|
||||||
|
|
||||||
|
| Komponente | Beschreibung |
|
||||||
|
|------------|--------------|
|
||||||
|
| `EmailClientTab.tsx` | Haupt-Tab mit Konto-Auswahl und Ordnern |
|
||||||
|
| `EmailList.tsx` | E-Mail-Liste mit Aktionen |
|
||||||
|
| `EmailDetail.tsx` | E-Mail-Ansicht mit Anhängen |
|
||||||
|
| `ComposeEmailModal.tsx` | Neue E-Mail / Antworten |
|
||||||
|
| `TrashEmailList.tsx` | Papierkorb-Verwaltung |
|
||||||
|
| `AssignToContractModal.tsx` | Vertragszuordnung |
|
||||||
|
| `ContractEmailsSection.tsx` | E-Mails in Vertragsansicht |
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Database Backups (can be large, keep folder structure)
|
||||||
|
prisma/backups/*
|
||||||
|
!prisma/backups/.gitkeep
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -4,4 +4,5 @@ export declare function getEmail(req: Request, res: Response): Promise<void>;
|
|||||||
export declare function createEmail(req: Request, res: Response): Promise<void>;
|
export declare function createEmail(req: Request, res: Response): Promise<void>;
|
||||||
export declare function updateEmail(req: Request, res: Response): Promise<void>;
|
export declare function updateEmail(req: Request, res: Response): Promise<void>;
|
||||||
export declare function deleteEmail(req: Request, res: Response): Promise<void>;
|
export declare function deleteEmail(req: Request, res: Response): Promise<void>;
|
||||||
|
export declare function resetPassword(req: Request, res: Response): Promise<void>;
|
||||||
//# sourceMappingURL=stressfreiEmail.controller.d.ts.map
|
//# sourceMappingURL=stressfreiEmail.controller.d.ts.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"stressfreiEmail.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/stressfreiEmail.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAI5C,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzE;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAc5E;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E"}
|
{"version":3,"file":"stressfreiEmail.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/stressfreiEmail.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAI5C,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzE;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAc5E;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB9E"}
|
||||||
@@ -38,6 +38,7 @@ exports.getEmail = getEmail;
|
|||||||
exports.createEmail = createEmail;
|
exports.createEmail = createEmail;
|
||||||
exports.updateEmail = updateEmail;
|
exports.updateEmail = updateEmail;
|
||||||
exports.deleteEmail = deleteEmail;
|
exports.deleteEmail = deleteEmail;
|
||||||
|
exports.resetPassword = resetPassword;
|
||||||
const stressfreiEmailService = __importStar(require("../services/stressfreiEmail.service.js"));
|
const stressfreiEmailService = __importStar(require("../services/stressfreiEmail.service.js"));
|
||||||
async function getEmailsByCustomer(req, res) {
|
async function getEmailsByCustomer(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -112,4 +113,27 @@ async function deleteEmail(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function resetPassword(req, res) {
|
||||||
|
try {
|
||||||
|
const result = await stressfreiEmailService.resetMailboxPassword(parseInt(req.params.id));
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { password: result.password },
|
||||||
|
message: 'Passwort wurde zurückgesetzt',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
//# sourceMappingURL=stressfreiEmail.controller.js.map
|
//# sourceMappingURL=stressfreiEmail.controller.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"stressfreiEmail.controller.js","sourceRoot":"","sources":["../../src/controllers/stressfreiEmail.controller.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"stressfreiEmail.controller.js","sourceRoot":"","sources":["../../src/controllers/stressfreiEmail.controller.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+2
@@ -27,6 +27,7 @@ const contractCategory_routes_js_1 = __importDefault(require("./routes/contractC
|
|||||||
const contractTask_routes_js_1 = __importDefault(require("./routes/contractTask.routes.js"));
|
const contractTask_routes_js_1 = __importDefault(require("./routes/contractTask.routes.js"));
|
||||||
const appSetting_routes_js_1 = __importDefault(require("./routes/appSetting.routes.js"));
|
const appSetting_routes_js_1 = __importDefault(require("./routes/appSetting.routes.js"));
|
||||||
const emailProvider_routes_js_1 = __importDefault(require("./routes/emailProvider.routes.js"));
|
const emailProvider_routes_js_1 = __importDefault(require("./routes/emailProvider.routes.js"));
|
||||||
|
const cachedEmail_routes_js_1 = __importDefault(require("./routes/cachedEmail.routes.js"));
|
||||||
dotenv_1.default.config();
|
dotenv_1.default.config();
|
||||||
const app = (0, express_1.default)();
|
const app = (0, express_1.default)();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
@@ -56,6 +57,7 @@ app.use('/api/contract-categories', contractCategory_routes_js_1.default);
|
|||||||
app.use('/api', contractTask_routes_js_1.default);
|
app.use('/api', contractTask_routes_js_1.default);
|
||||||
app.use('/api/settings', appSetting_routes_js_1.default);
|
app.use('/api/settings', appSetting_routes_js_1.default);
|
||||||
app.use('/api/email-providers', emailProvider_routes_js_1.default);
|
app.use('/api/email-providers', emailProvider_routes_js_1.default);
|
||||||
|
app.use('/api', cachedEmail_routes_js_1.default);
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { Response, NextFunction } from 'express';
|
import { Response, NextFunction } from 'express';
|
||||||
import { AuthRequest } from '../types/index.js';
|
import { AuthRequest } from '../types/index.js';
|
||||||
export declare function authenticate(req: AuthRequest, res: Response, next: NextFunction): void;
|
export declare function authenticate(req: AuthRequest, res: Response, next: NextFunction): Promise<void>;
|
||||||
export declare function requirePermission(...requiredPermissions: string[]): (req: AuthRequest, res: Response, next: NextFunction) => void;
|
export declare function requirePermission(...requiredPermissions: string[]): (req: AuthRequest, res: Response, next: NextFunction) => void;
|
||||||
export declare function requireCustomerAccess(req: AuthRequest, res: Response, next: NextFunction): void;
|
export declare function requireCustomerAccess(req: AuthRequest, res: Response, next: NextFunction): void;
|
||||||
//# sourceMappingURL=auth.d.ts.map
|
//# sourceMappingURL=auth.d.ts.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAE5D,wBAAgB,YAAY,CAC1B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CAoBN;AAED,wBAAgB,iBAAiB,CAAC,GAAG,mBAAmB,EAAE,MAAM,EAAE,IACxD,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAuBnE;AAGD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CA4BN"}
|
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAC;AAI5D,wBAAsB,YAAY,CAChC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,CAAC,CAuDf;AAED,wBAAgB,iBAAiB,CAAC,GAAG,mBAAmB,EAAE,MAAM,EAAE,IACxD,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAuBnE;AAGD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,IAAI,CA4BN"}
|
||||||
Vendored
+36
-3
@@ -7,15 +7,48 @@ exports.authenticate = authenticate;
|
|||||||
exports.requirePermission = requirePermission;
|
exports.requirePermission = requirePermission;
|
||||||
exports.requireCustomerAccess = requireCustomerAccess;
|
exports.requireCustomerAccess = requireCustomerAccess;
|
||||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||||
function authenticate(req, res, next) {
|
const client_1 = require("@prisma/client");
|
||||||
|
const prisma = new client_1.PrismaClient();
|
||||||
|
async function authenticate(req, res, next) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// Token aus Header oder Query-Parameter (für Downloads)
|
||||||
|
let token = null;
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
token = authHeader.split(' ')[1];
|
||||||
|
}
|
||||||
|
else if (req.query.token && typeof req.query.token === 'string') {
|
||||||
|
// Fallback für Downloads: Token als Query-Parameter
|
||||||
|
token = req.query.token;
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = authHeader.split(' ')[1];
|
|
||||||
try {
|
try {
|
||||||
const decoded = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET || 'fallback-secret');
|
const decoded = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET || 'fallback-secret');
|
||||||
|
// Prüfen ob Token durch Rechteänderung invalidiert wurde (nur für Mitarbeiter)
|
||||||
|
if (decoded.userId && decoded.iat) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.userId },
|
||||||
|
select: { tokenInvalidatedAt: true, isActive: true },
|
||||||
|
});
|
||||||
|
// Benutzer nicht gefunden oder deaktiviert
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
res.status(401).json({ success: false, error: 'Benutzer nicht mehr aktiv' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Token wurde vor der Invalidierung ausgestellt
|
||||||
|
if (user.tokenInvalidatedAt) {
|
||||||
|
const tokenIssuedAt = decoded.iat * 1000; // iat ist in Sekunden, Date ist in Millisekunden
|
||||||
|
if (tokenIssuedAt < user.tokenInvalidatedAt.getTime()) {
|
||||||
|
res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ihre Berechtigungen wurden geändert. Bitte melden Sie sich erneut an.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
req.user = decoded;
|
req.user = decoded;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"appSetting.routes.d.ts","sourceRoot":"","sources":["../../src/routes/appSetting.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwBxB,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"appSetting.routes.d.ts","sourceRoot":"","sources":["../../src/routes/appSetting.routes.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmFxB,eAAe,MAAM,CAAC"}
|
||||||
+33
@@ -32,10 +32,28 @@ var __importStar = (this && this.__importStar) || (function () {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
|
const multer_1 = __importDefault(require("multer"));
|
||||||
const appSettingController = __importStar(require("../controllers/appSetting.controller.js"));
|
const appSettingController = __importStar(require("../controllers/appSetting.controller.js"));
|
||||||
|
const backupController = __importStar(require("../controllers/backup.controller.js"));
|
||||||
const auth_js_1 = require("../middleware/auth.js");
|
const auth_js_1 = require("../middleware/auth.js");
|
||||||
|
// Multer für Backup-Upload (in Memory speichern)
|
||||||
|
const backupUpload = (0, multer_1.default)({
|
||||||
|
storage: multer_1.default.memoryStorage(),
|
||||||
|
limits: { fileSize: 500 * 1024 * 1024 }, // 500MB max
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype === 'application/zip' || file.originalname.endsWith('.zip')) {
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cb(new Error('Nur ZIP-Dateien sind erlaubt'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
||||||
router.get('/public', auth_js_1.authenticate, appSettingController.getPublicSettings);
|
router.get('/public', auth_js_1.authenticate, appSettingController.getPublicSettings);
|
||||||
@@ -45,5 +63,20 @@ router.get('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settin
|
|||||||
router.put('/:key', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), appSettingController.updateSetting);
|
router.put('/:key', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), appSettingController.updateSetting);
|
||||||
// Mehrere Einstellungen aktualisieren (nur Admin)
|
// Mehrere Einstellungen aktualisieren (nur Admin)
|
||||||
router.put('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), appSettingController.updateSettings);
|
router.put('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), appSettingController.updateSettings);
|
||||||
|
// ==================== BACKUP & RESTORE ====================
|
||||||
|
// Liste aller Backups
|
||||||
|
router.get('/backups', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.listBackups);
|
||||||
|
// Neues Backup erstellen
|
||||||
|
router.post('/backup', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.createBackup);
|
||||||
|
// Backup wiederherstellen
|
||||||
|
router.post('/backup/:name/restore', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.restoreBackup);
|
||||||
|
// Backup löschen
|
||||||
|
router.delete('/backup/:name', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.deleteBackup);
|
||||||
|
// Backup als ZIP herunterladen
|
||||||
|
router.get('/backup/:name/download', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.downloadBackup);
|
||||||
|
// Backup-ZIP hochladen
|
||||||
|
router.post('/backup/upload', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupUpload.single('backup'), backupController.uploadBackup);
|
||||||
|
// Werkseinstellungen (alles löschen)
|
||||||
|
router.post('/factory-reset', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('settings:update'), backupController.factoryReset);
|
||||||
exports.default = router;
|
exports.default = router;
|
||||||
//# sourceMappingURL=appSetting.routes.js.map
|
//# sourceMappingURL=appSetting.routes.js.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"appSetting.routes.js","sourceRoot":"","sources":["../../src/routes/appSetting.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,8FAAgF;AAChF,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,gFAAgF;AAChF,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,sBAAY,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;AAE5E,iCAAiC;AACjC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,eAAe,CAAC,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAC;AAEvG,iDAAiD;AACjD,MAAM,CAAC,GAAG,CACR,OAAO,EACP,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,oBAAoB,CAAC,aAAa,CACnC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,GAAG,CACR,GAAG,EACH,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,oBAAoB,CAAC,cAAc,CACpC,CAAC;AAEF,kBAAe,MAAM,CAAC"}
|
{"version":3,"file":"appSetting.routes.js","sourceRoot":"","sources":["../../src/routes/appSetting.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,oDAA4B;AAC5B,8FAAgF;AAChF,sFAAwE;AACxE,mDAAwE;AAExE,iDAAiD;AACjD,MAAM,YAAY,GAAG,IAAA,gBAAM,EAAC;IAC1B,OAAO,EAAE,gBAAM,CAAC,aAAa,EAAE;IAC/B,MAAM,EAAE,EAAE,QAAQ,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,YAAY;IACrD,UAAU,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAC5B,IAAI,IAAI,CAAC,QAAQ,KAAK,iBAAiB,IAAI,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9E,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjB,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,gFAAgF;AAChF,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,sBAAY,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;AAE5E,iCAAiC;AACjC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,eAAe,CAAC,EAAE,oBAAoB,CAAC,cAAc,CAAC,CAAC;AAEvG,iDAAiD;AACjD,MAAM,CAAC,GAAG,CACR,OAAO,EACP,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,oBAAoB,CAAC,aAAa,CACnC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,GAAG,CACR,GAAG,EACH,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,oBAAoB,CAAC,cAAc,CACpC,CAAC;AAEF,6DAA6D;AAE7D,sBAAsB;AACtB,MAAM,CAAC,GAAG,CACR,UAAU,EACV,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,WAAW,CAC7B,CAAC;AAEF,yBAAyB;AACzB,MAAM,CAAC,IAAI,CACT,SAAS,EACT,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,YAAY,CAC9B,CAAC;AAEF,0BAA0B;AAC1B,MAAM,CAAC,IAAI,CACT,uBAAuB,EACvB,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,aAAa,CAC/B,CAAC;AAEF,iBAAiB;AACjB,MAAM,CAAC,MAAM,CACX,eAAe,EACf,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,YAAY,CAC9B,CAAC;AAEF,+BAA+B;AAC/B,MAAM,CAAC,GAAG,CACR,wBAAwB,EACxB,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,cAAc,CAChC,CAAC;AAEF,uBAAuB;AACvB,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC7B,gBAAgB,CAAC,YAAY,CAC9B,CAAC;AAEF,qCAAqC;AACrC,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,sBAAY,EACZ,IAAA,2BAAiB,EAAC,iBAAiB,CAAC,EACpC,gBAAgB,CAAC,YAAY,CAC9B,CAAC;AAEF,kBAAe,MAAM,CAAC"}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"contractCategory.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contractCategory.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAQxB,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"contractCategory.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contractCategory.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAWxB,eAAe,MAAM,CAAC"}
|
||||||
+5
-3
@@ -37,10 +37,12 @@ const express_1 = require("express");
|
|||||||
const contractCategoryController = __importStar(require("../controllers/contractCategory.controller.js"));
|
const contractCategoryController = __importStar(require("../controllers/contractCategory.controller.js"));
|
||||||
const auth_js_1 = require("../middleware/auth.js");
|
const auth_js_1 = require("../middleware/auth.js");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
|
// Lesen für alle authentifizierten Benutzer
|
||||||
router.get('/', auth_js_1.authenticate, contractCategoryController.getContractCategories);
|
router.get('/', auth_js_1.authenticate, contractCategoryController.getContractCategories);
|
||||||
router.post('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('platforms:create'), contractCategoryController.createContractCategory);
|
|
||||||
router.get('/:id', auth_js_1.authenticate, contractCategoryController.getContractCategory);
|
router.get('/:id', auth_js_1.authenticate, contractCategoryController.getContractCategory);
|
||||||
router.put('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('platforms:update'), contractCategoryController.updateContractCategory);
|
// Ändern/Löschen nur mit Entwickler-Berechtigung (Vertragstypen erfordern Formular-Anpassungen)
|
||||||
router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('platforms:delete'), contractCategoryController.deleteContractCategory);
|
router.post('/', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('developer:access'), contractCategoryController.createContractCategory);
|
||||||
|
router.put('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('developer:access'), contractCategoryController.updateContractCategory);
|
||||||
|
router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('developer:access'), contractCategoryController.deleteContractCategory);
|
||||||
exports.default = router;
|
exports.default = router;
|
||||||
//# sourceMappingURL=contractCategory.routes.js.map
|
//# sourceMappingURL=contractCategory.routes.js.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"contractCategory.routes.js","sourceRoot":"","sources":["../../src/routes/contractCategory.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0GAA4F;AAC5F,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,0BAA0B,CAAC,qBAAqB,CAAC,CAAC;AAChF,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AACzH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;AACjF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AAC3H,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AAE9H,kBAAe,MAAM,CAAC"}
|
{"version":3,"file":"contractCategory.routes.js","sourceRoot":"","sources":["../../src/routes/contractCategory.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0GAA4F;AAC5F,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,4CAA4C;AAC5C,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,0BAA0B,CAAC,qBAAqB,CAAC,CAAC;AAChF,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,0BAA0B,CAAC,mBAAmB,CAAC,CAAC;AAEjF,gGAAgG;AAChG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AACzH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AAC3H,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,0BAA0B,CAAC,sBAAsB,CAAC,CAAC;AAE9H,kBAAe,MAAM,CAAC"}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"stressfreiEmail.routes.d.ts","sourceRoot":"","sources":["../../src/routes/stressfreiEmail.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAOxB,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"stressfreiEmail.routes.d.ts","sourceRoot":"","sources":["../../src/routes/stressfreiEmail.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAUxB,eAAe,MAAM,CAAC"}
|
||||||
@@ -41,5 +41,7 @@ const router = (0, express_1.Router)();
|
|||||||
router.get('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:read'), stressfreiEmailController.getEmail);
|
router.get('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:read'), stressfreiEmailController.getEmail);
|
||||||
router.put('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), stressfreiEmailController.updateEmail);
|
router.put('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), stressfreiEmailController.updateEmail);
|
||||||
router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:delete'), stressfreiEmailController.deleteEmail);
|
router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:delete'), stressfreiEmailController.deleteEmail);
|
||||||
|
// Passwort zurücksetzen (generiert neues Passwort und setzt es beim Provider)
|
||||||
|
router.post('/:id/reset-password', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('customers:update'), stressfreiEmailController.resetPassword);
|
||||||
exports.default = router;
|
exports.default = router;
|
||||||
//# sourceMappingURL=stressfreiEmail.routes.js.map
|
//# sourceMappingURL=stressfreiEmail.routes.js.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"stressfreiEmail.routes.js","sourceRoot":"","sources":["../../src/routes/stressfreiEmail.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,wGAA0F;AAC1F,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,sCAAsC;AACtC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,yBAAyB,CAAC,QAAQ,CAAC,CAAC;AAC1G,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAC/G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAElH,kBAAe,MAAM,CAAC"}
|
{"version":3,"file":"stressfreiEmail.routes.js","sourceRoot":"","sources":["../../src/routes/stressfreiEmail.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,wGAA0F;AAC1F,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,sCAAsC;AACtC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,yBAAyB,CAAC,QAAQ,CAAC,CAAC;AAC1G,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAC/G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAElH,8EAA8E;AAC9E,MAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,yBAAyB,CAAC,aAAa,CAAC,CAAC;AAEjI,kBAAe,MAAM,CAAC"}
|
||||||
+14
@@ -247,10 +247,12 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
carInsuranceDetails: {
|
carInsuranceDetails: {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -511,6 +513,11 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||||||
portalUsername: string | null;
|
portalUsername: string | null;
|
||||||
stressfreiEmailId: number | null;
|
stressfreiEmailId: number | null;
|
||||||
}) | null;
|
}) | null;
|
||||||
|
followUpContract: {
|
||||||
|
id: number;
|
||||||
|
status: import(".prisma/client").$Enums.ContractStatus;
|
||||||
|
contractNumber: string;
|
||||||
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
@@ -936,10 +943,12 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
carInsuranceDetails: {
|
carInsuranceDetails: {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -1200,6 +1209,11 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||||||
portalUsername: string | null;
|
portalUsername: string | null;
|
||||||
stressfreiEmailId: number | null;
|
stressfreiEmailId: number | null;
|
||||||
}) | null;
|
}) | null;
|
||||||
|
followUpContract: {
|
||||||
|
id: number;
|
||||||
|
status: import(".prisma/client").$Enums.ContractStatus;
|
||||||
|
contractNumber: string;
|
||||||
|
} | null;
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+32
-1
@@ -27,8 +27,14 @@ async function getAllContracts(filters) {
|
|||||||
}
|
}
|
||||||
if (type)
|
if (type)
|
||||||
where.type = type;
|
where.type = type;
|
||||||
if (status)
|
// Status-Filter: Deaktivierte Verträge standardmäßig ausblenden
|
||||||
|
if (status) {
|
||||||
where.status = status;
|
where.status = status;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Wenn kein Status-Filter gesetzt, alle außer DEACTIVATED anzeigen
|
||||||
|
where.status = { not: client_1.ContractStatus.DEACTIVATED };
|
||||||
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
// Basis-Vertragsfelder
|
// Basis-Vertragsfelder
|
||||||
@@ -125,6 +131,9 @@ async function getContractById(id, decryptPassword = false) {
|
|||||||
tvDetails: true,
|
tvDetails: true,
|
||||||
carInsuranceDetails: true,
|
carInsuranceDetails: true,
|
||||||
stressfreiEmail: true,
|
stressfreiEmail: true,
|
||||||
|
followUpContract: {
|
||||||
|
select: { id: true, contractNumber: true, status: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!contract)
|
if (!contract)
|
||||||
@@ -412,6 +421,19 @@ async function updateContract(id, data) {
|
|||||||
return getContractById(id);
|
return getContractById(id);
|
||||||
}
|
}
|
||||||
async function deleteContract(id) {
|
async function deleteContract(id) {
|
||||||
|
// Vertragskette erhalten beim Löschen:
|
||||||
|
// Wenn A → B → C und B gelöscht wird, soll C direkt auf A zeigen (A → C)
|
||||||
|
// 1. Zu löschenden Vertrag holen um dessen Vorgänger zu kennen
|
||||||
|
const contractToDelete = await prisma.contract.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { previousContractId: true },
|
||||||
|
});
|
||||||
|
// 2. Folgevertrag(e) mit dem Vorgänger des gelöschten Vertrags verbinden
|
||||||
|
// So bleibt die Kette erhalten: A → B → C wird zu A → C
|
||||||
|
await prisma.contract.updateMany({
|
||||||
|
where: { previousContractId: id },
|
||||||
|
data: { previousContractId: contractToDelete?.previousContractId ?? null },
|
||||||
|
});
|
||||||
return prisma.contract.delete({ where: { id } });
|
return prisma.contract.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
async function createFollowUpContract(previousContractId) {
|
async function createFollowUpContract(previousContractId) {
|
||||||
@@ -419,6 +441,14 @@ async function createFollowUpContract(previousContractId) {
|
|||||||
if (!previousContract) {
|
if (!previousContract) {
|
||||||
throw new Error('Vorgängervertrag nicht gefunden');
|
throw new Error('Vorgängervertrag nicht gefunden');
|
||||||
}
|
}
|
||||||
|
// Prüfen ob bereits ein Folgevertrag existiert
|
||||||
|
const existingFollowUp = await prisma.contract.findFirst({
|
||||||
|
where: { previousContractId },
|
||||||
|
select: { id: true, contractNumber: true },
|
||||||
|
});
|
||||||
|
if (existingFollowUp) {
|
||||||
|
throw new Error(`Es existiert bereits ein Folgevertrag: ${existingFollowUp.contractNumber}`);
|
||||||
|
}
|
||||||
// Copy data but exclude provider credentials and some fields
|
// Copy data but exclude provider credentials and some fields
|
||||||
const newContractData = {
|
const newContractData = {
|
||||||
customerId: previousContract.customerId,
|
customerId: previousContract.customerId,
|
||||||
@@ -441,6 +471,7 @@ async function createFollowUpContract(previousContractId) {
|
|||||||
annualConsumption: previousContract.energyDetails.annualConsumption ?? undefined,
|
annualConsumption: previousContract.energyDetails.annualConsumption ?? undefined,
|
||||||
basePrice: previousContract.energyDetails.basePrice ?? undefined,
|
basePrice: previousContract.energyDetails.basePrice ?? undefined,
|
||||||
unitPrice: previousContract.energyDetails.unitPrice ?? undefined,
|
unitPrice: previousContract.energyDetails.unitPrice ?? undefined,
|
||||||
|
bonus: previousContract.energyDetails.bonus ?? undefined,
|
||||||
previousProviderName: previousContract.providerName ?? undefined,
|
previousProviderName: previousContract.providerName ?? undefined,
|
||||||
previousCustomerNumber: previousContract.customerNumberAtProvider ?? undefined,
|
previousCustomerNumber: previousContract.customerNumberAtProvider ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+2
@@ -128,10 +128,12 @@ export declare function getCustomerById(id: number): Promise<({
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
}[];
|
}[];
|
||||||
contracts: ({
|
contracts: ({
|
||||||
address: {
|
address: {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+4
@@ -108,6 +108,10 @@ async function getCustomerById(id) {
|
|||||||
},
|
},
|
||||||
stressfreiEmails: { orderBy: { isActive: 'desc' } },
|
stressfreiEmails: { orderBy: { isActive: 'desc' } },
|
||||||
contracts: {
|
contracts: {
|
||||||
|
where: {
|
||||||
|
// Deaktivierte Verträge ausblenden
|
||||||
|
status: { not: client_1.ContractStatus.DEACTIVATED },
|
||||||
|
},
|
||||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||||
include: {
|
include: {
|
||||||
address: true,
|
address: true,
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import { EmailExistsResult, EmailOperationResult } from './types.js';
|
import { EmailExistsResult, EmailOperationResult, MailEncryption } from './types.js';
|
||||||
export declare function getAllProviderConfigs(): Promise<{
|
export declare function getAllProviderConfigs(): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -13,6 +13,13 @@ export declare function getAllProviderConfigs(): Promise<{
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
}[]>;
|
}[]>;
|
||||||
export declare function getProviderConfigById(id: number): Promise<{
|
export declare function getProviderConfigById(id: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -28,6 +35,13 @@ export declare function getProviderConfigById(id: number): Promise<{
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
} | null>;
|
} | null>;
|
||||||
export declare function getDefaultProviderConfig(): Promise<{
|
export declare function getDefaultProviderConfig(): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -43,6 +57,13 @@ export declare function getDefaultProviderConfig(): Promise<{
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
} | null>;
|
} | null>;
|
||||||
export declare function getActiveProviderConfig(): Promise<{
|
export declare function getActiveProviderConfig(): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -58,6 +79,13 @@ export declare function getActiveProviderConfig(): Promise<{
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
} | null>;
|
} | null>;
|
||||||
export interface CreateProviderConfigData {
|
export interface CreateProviderConfigData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -68,6 +96,9 @@ export interface CreateProviderConfigData {
|
|||||||
password?: string;
|
password?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail?: string;
|
defaultForwardEmail?: string;
|
||||||
|
imapEncryption?: MailEncryption;
|
||||||
|
smtpEncryption?: MailEncryption;
|
||||||
|
allowSelfSignedCerts?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
@@ -85,6 +116,13 @@ export declare function createProviderConfig(data: CreateProviderConfigData): Pr
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
}>;
|
}>;
|
||||||
export declare function updateProviderConfig(id: number, data: Partial<CreateProviderConfigData>): Promise<{
|
export declare function updateProviderConfig(id: number, data: Partial<CreateProviderConfigData>): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -100,6 +138,13 @@ export declare function updateProviderConfig(id: number, data: Partial<CreatePro
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
}>;
|
}>;
|
||||||
export declare function deleteProviderConfig(id: number): Promise<{
|
export declare function deleteProviderConfig(id: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -115,9 +160,32 @@ export declare function deleteProviderConfig(id: number): Promise<{
|
|||||||
passwordEncrypted: string | null;
|
passwordEncrypted: string | null;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail: string | null;
|
defaultForwardEmail: string | null;
|
||||||
|
imapServer: string | null;
|
||||||
|
imapPort: number | null;
|
||||||
|
smtpServer: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
imapEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
smtpEncryption: import(".prisma/client").$Enums.MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
}>;
|
}>;
|
||||||
export declare function checkEmailExists(localPart: string): Promise<EmailExistsResult>;
|
export declare function checkEmailExists(localPart: string): Promise<EmailExistsResult>;
|
||||||
export declare function provisionEmail(localPart: string, customerEmail: string): Promise<EmailOperationResult>;
|
export declare function provisionEmail(localPart: string, customerEmail: string): Promise<EmailOperationResult>;
|
||||||
|
export declare function provisionEmailWithMailbox(localPart: string, customerEmail: string, password: string): Promise<EmailOperationResult & {
|
||||||
|
email?: string;
|
||||||
|
}>;
|
||||||
|
export declare function enableMailboxForExistingEmail(localPart: string, password: string): Promise<EmailOperationResult>;
|
||||||
|
export declare function updateMailboxPassword(localPart: string, password: string): Promise<EmailOperationResult>;
|
||||||
|
export interface ImapSmtpSettings {
|
||||||
|
imapServer: string;
|
||||||
|
imapPort: number;
|
||||||
|
imapEncryption: MailEncryption;
|
||||||
|
smtpServer: string;
|
||||||
|
smtpPort: number;
|
||||||
|
smtpEncryption: MailEncryption;
|
||||||
|
allowSelfSignedCerts: boolean;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
export declare function getImapSmtpSettings(): Promise<ImapSmtpSettings | null>;
|
||||||
export declare function deprovisionEmail(localPart: string): Promise<EmailOperationResult>;
|
export declare function deprovisionEmail(localPart: string): Promise<EmailOperationResult>;
|
||||||
export declare function renameProvisionedEmail(oldLocalPart: string, newLocalPart: string): Promise<EmailOperationResult>;
|
export declare function renameProvisionedEmail(oldLocalPart: string, newLocalPart: string): Promise<EmailOperationResult>;
|
||||||
export declare function getProviderDomain(): Promise<string | null>;
|
export declare function getProviderDomain(): Promise<string | null>;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"emailProviderService.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/emailProviderService.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,iBAAiB,EACjB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAuBpB,wBAAsB,qBAAqB;;;;;;;;;;;;;;KAI1C;AAED,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;UAIrD;AAED,wBAAsB,wBAAwB;;;;;;;;;;;;;;UAI7C;AAED,wBAAsB,uBAAuB;;;;;;;;;;;;;;UAQ5C;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,wBAAwB;;;;;;;;;;;;;;GA2BxE;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC,wBAAwB,CAAC;;;;;;;;;;;;;;GAsCxC;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;GAIpD;AAwCD,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAQpF;AAGD,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,oBAAoB,CAAC,CAoC/B;AAGD,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAWvF;AAGD,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAW/B;AAGD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGhE;AA8DD,wBAAsB,sBAAsB,CAAC,OAAO,CAAC,EAAE;IACrD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE;QACT,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;QACzC,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6BhC"}
|
{"version":3,"file":"emailProviderService.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/emailProviderService.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -10,6 +10,10 @@ exports.updateProviderConfig = updateProviderConfig;
|
|||||||
exports.deleteProviderConfig = deleteProviderConfig;
|
exports.deleteProviderConfig = deleteProviderConfig;
|
||||||
exports.checkEmailExists = checkEmailExists;
|
exports.checkEmailExists = checkEmailExists;
|
||||||
exports.provisionEmail = provisionEmail;
|
exports.provisionEmail = provisionEmail;
|
||||||
|
exports.provisionEmailWithMailbox = provisionEmailWithMailbox;
|
||||||
|
exports.enableMailboxForExistingEmail = enableMailboxForExistingEmail;
|
||||||
|
exports.updateMailboxPassword = updateMailboxPassword;
|
||||||
|
exports.getImapSmtpSettings = getImapSmtpSettings;
|
||||||
exports.deprovisionEmail = deprovisionEmail;
|
exports.deprovisionEmail = deprovisionEmail;
|
||||||
exports.renameProvisionedEmail = renameProvisionedEmail;
|
exports.renameProvisionedEmail = renameProvisionedEmail;
|
||||||
exports.getProviderDomain = getProviderDomain;
|
exports.getProviderDomain = getProviderDomain;
|
||||||
@@ -79,6 +83,9 @@ async function createProviderConfig(data) {
|
|||||||
passwordEncrypted,
|
passwordEncrypted,
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
defaultForwardEmail: data.defaultForwardEmail || null,
|
defaultForwardEmail: data.defaultForwardEmail || null,
|
||||||
|
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||||
|
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||||
|
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||||
isActive: data.isActive ?? true,
|
isActive: data.isActive ?? true,
|
||||||
isDefault: data.isDefault ?? false,
|
isDefault: data.isDefault ?? false,
|
||||||
},
|
},
|
||||||
@@ -107,6 +114,12 @@ async function updateProviderConfig(id, data) {
|
|||||||
updateData.domain = data.domain;
|
updateData.domain = data.domain;
|
||||||
if (data.defaultForwardEmail !== undefined)
|
if (data.defaultForwardEmail !== undefined)
|
||||||
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
|
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
|
||||||
|
if (data.imapEncryption !== undefined)
|
||||||
|
updateData.imapEncryption = data.imapEncryption;
|
||||||
|
if (data.smtpEncryption !== undefined)
|
||||||
|
updateData.smtpEncryption = data.smtpEncryption;
|
||||||
|
if (data.allowSelfSignedCerts !== undefined)
|
||||||
|
updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
|
||||||
if (data.isActive !== undefined)
|
if (data.isActive !== undefined)
|
||||||
updateData.isActive = data.isActive;
|
updateData.isActive = data.isActive;
|
||||||
if (data.isDefault !== undefined)
|
if (data.isDefault !== undefined)
|
||||||
@@ -159,6 +172,13 @@ async function getProviderInstance() {
|
|||||||
password,
|
password,
|
||||||
domain: dbConfig.domain,
|
domain: dbConfig.domain,
|
||||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||||
|
imapServer: dbConfig.imapServer || undefined,
|
||||||
|
imapPort: dbConfig.imapPort || undefined,
|
||||||
|
smtpServer: dbConfig.smtpServer || undefined,
|
||||||
|
smtpPort: dbConfig.smtpPort || undefined,
|
||||||
|
imapEncryption: dbConfig.imapEncryption,
|
||||||
|
smtpEncryption: dbConfig.smtpEncryption,
|
||||||
|
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
|
||||||
isActive: dbConfig.isActive,
|
isActive: dbConfig.isActive,
|
||||||
isDefault: dbConfig.isDefault,
|
isDefault: dbConfig.isDefault,
|
||||||
};
|
};
|
||||||
@@ -209,6 +229,136 @@ async function provisionEmail(localPart, customerEmail) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// E-Mail mit echter Mailbox erstellen (IMAP/SMTP-Zugang)
|
||||||
|
async function provisionEmailWithMailbox(localPart, customerEmail, password) {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
const config = await getActiveProviderConfig();
|
||||||
|
// Weiterleitungsziele zusammenstellen
|
||||||
|
const forwardTargets = [customerEmail];
|
||||||
|
// Unsere eigene Weiterleitungsadresse hinzufügen falls konfiguriert
|
||||||
|
if (config?.defaultForwardEmail) {
|
||||||
|
forwardTargets.push(config.defaultForwardEmail);
|
||||||
|
}
|
||||||
|
// Prüfen ob existiert
|
||||||
|
const exists = await provider.emailExists(localPart);
|
||||||
|
if (exists.exists) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `E-Mail ${exists.email} existiert bereits`,
|
||||||
|
email: exists.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Mit Mailbox erstellen
|
||||||
|
const result = await provider.createEmailWithMailbox({
|
||||||
|
localPart,
|
||||||
|
forwardTargets,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mailbox für existierende E-Mail-Weiterleitung aktivieren
|
||||||
|
async function enableMailboxForExistingEmail(localPart, password) {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
const result = await provider.enableMailboxForExisting({
|
||||||
|
localPart,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mailbox-Passwort beim Provider aktualisieren
|
||||||
|
async function updateMailboxPassword(localPart, password) {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
const result = await provider.updateMailboxPassword({
|
||||||
|
localPart,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getImapSmtpSettings() {
|
||||||
|
const config = await getActiveProviderConfig();
|
||||||
|
if (!config)
|
||||||
|
return null;
|
||||||
|
// Default-Server: Hostname aus der apiUrl extrahieren (z.B. rs001871.fastrootserver.de aus https://rs001871.fastrootserver.de:8443)
|
||||||
|
// Der Plesk-Server ist gleichzeitig der Mail-Server
|
||||||
|
let defaultServer;
|
||||||
|
try {
|
||||||
|
const url = new URL(config.apiUrl);
|
||||||
|
defaultServer = url.hostname;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Fallback falls apiUrl ungültig
|
||||||
|
defaultServer = `mail.${config.domain}`;
|
||||||
|
}
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
const imapEncryption = (config.imapEncryption ?? 'SSL');
|
||||||
|
const smtpEncryption = (config.smtpEncryption ?? 'SSL');
|
||||||
|
// Ports basierend auf Verschlüsselung berechnen:
|
||||||
|
// SSL: IMAP 993, SMTP 465
|
||||||
|
// STARTTLS: IMAP 143, SMTP 587
|
||||||
|
// NONE: IMAP 143, SMTP 25
|
||||||
|
//
|
||||||
|
// Standard-Ports werden IMMER basierend auf Verschlüsselung berechnet.
|
||||||
|
// Nur benutzerdefinierte Ports (nicht 993/143/465/587/25) werden aus der DB übernommen.
|
||||||
|
const getImapPort = (enc, storedPort) => {
|
||||||
|
const standardPorts = [993, 143];
|
||||||
|
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
|
||||||
|
if (storedPort && !standardPorts.includes(storedPort)) {
|
||||||
|
return storedPort;
|
||||||
|
}
|
||||||
|
// Sonst basierend auf Verschlüsselung
|
||||||
|
return enc === 'SSL' ? 993 : 143;
|
||||||
|
};
|
||||||
|
const getSmtpPort = (enc, storedPort) => {
|
||||||
|
const standardPorts = [465, 587, 25];
|
||||||
|
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
|
||||||
|
if (storedPort && !standardPorts.includes(storedPort)) {
|
||||||
|
return storedPort;
|
||||||
|
}
|
||||||
|
// Sonst basierend auf Verschlüsselung
|
||||||
|
if (enc === 'SSL')
|
||||||
|
return 465;
|
||||||
|
if (enc === 'STARTTLS')
|
||||||
|
return 587;
|
||||||
|
return 25; // NONE
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
imapServer: config.imapServer || defaultServer,
|
||||||
|
imapPort: getImapPort(imapEncryption, config.imapPort),
|
||||||
|
imapEncryption,
|
||||||
|
smtpServer: config.smtpServer || defaultServer,
|
||||||
|
smtpPort: getSmtpPort(smtpEncryption, config.smtpPort),
|
||||||
|
smtpEncryption,
|
||||||
|
allowSelfSignedCerts: config.allowSelfSignedCerts ?? false,
|
||||||
|
domain: config.domain,
|
||||||
|
};
|
||||||
|
}
|
||||||
// E-Mail löschen
|
// E-Mail löschen
|
||||||
async function deprovisionEmail(localPart) {
|
async function deprovisionEmail(localPart) {
|
||||||
try {
|
try {
|
||||||
@@ -284,6 +434,13 @@ async function getProviderInstanceById(id) {
|
|||||||
password,
|
password,
|
||||||
domain: dbConfig.domain,
|
domain: dbConfig.domain,
|
||||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||||
|
imapServer: dbConfig.imapServer || undefined,
|
||||||
|
imapPort: dbConfig.imapPort || undefined,
|
||||||
|
smtpServer: dbConfig.smtpServer || undefined,
|
||||||
|
smtpPort: dbConfig.smtpPort || undefined,
|
||||||
|
imapEncryption: dbConfig.imapEncryption,
|
||||||
|
smtpEncryption: dbConfig.smtpEncryption,
|
||||||
|
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
|
||||||
isActive: dbConfig.isActive,
|
isActive: dbConfig.isActive,
|
||||||
isDefault: dbConfig.isDefault,
|
isDefault: dbConfig.isDefault,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+4
-1
@@ -1,4 +1,4 @@
|
|||||||
import { IEmailProvider, EmailProviderConfig, EmailExistsResult, EmailOperationResult, CreateEmailParams, RenameEmailParams } from './types.js';
|
import { IEmailProvider, EmailProviderConfig, EmailExistsResult, EmailOperationResult, CreateEmailParams, CreateEmailWithMailboxParams, CreateEmailWithMailboxResult, EnableMailboxParams, UpdateMailboxPasswordParams, RenameEmailParams } from './types.js';
|
||||||
export declare class PleskEmailProvider implements IEmailProvider {
|
export declare class PleskEmailProvider implements IEmailProvider {
|
||||||
readonly type = "PLESK";
|
readonly type = "PLESK";
|
||||||
private config;
|
private config;
|
||||||
@@ -8,6 +8,9 @@ export declare class PleskEmailProvider implements IEmailProvider {
|
|||||||
testConnection(): Promise<void>;
|
testConnection(): Promise<void>;
|
||||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||||
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
||||||
|
createEmailWithMailbox(params: CreateEmailWithMailboxParams): Promise<CreateEmailWithMailboxResult>;
|
||||||
|
enableMailboxForExisting(params: EnableMailboxParams): Promise<EmailOperationResult>;
|
||||||
|
updateMailboxPassword(params: UpdateMailboxPasswordParams): Promise<EmailOperationResult>;
|
||||||
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
||||||
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
||||||
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"pleskProvider.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/pleskProvider.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,kBAAmB,YAAW,cAAc;IACvD,QAAQ,CAAC,IAAI,WAAW;IACxB,OAAO,CAAC,MAAM,CAAsB;gBAExB,MAAM,EAAE,mBAAmB;IAKvC,OAAO,KAAK,OAAO,GAGlB;YAGa,OAAO;IAsFf,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB/B,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA8C1D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAuCrE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAgC7D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA2CrE,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,oBAAoB,CAAC;CAmCjC"}
|
{"version":3,"file":"pleskProvider.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/pleskProvider.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,4BAA4B,EAC5B,4BAA4B,EAC5B,mBAAmB,EACnB,2BAA2B,EAC3B,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAiBpB,qBAAa,kBAAmB,YAAW,cAAc;IACvD,QAAQ,CAAC,IAAI,WAAW;IACxB,OAAO,CAAC,MAAM,CAAsB;gBAExB,MAAM,EAAE,mBAAmB;IAKvC,OAAO,KAAK,OAAO,GAGlB;YAGa,OAAO;IAsFf,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB/B,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyD1D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAwCrE,sBAAsB,CAAC,MAAM,EAAE,4BAA4B,GAAG,OAAO,CAAC,4BAA4B,CAAC;IA2CnG,wBAAwB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAwCpF,qBAAqB,CAAC,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAsCzF,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAgC7D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA2CrE,oBAAoB,CACxB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,oBAAoB,CAAC;CAoCjC"}
|
||||||
+125
-2
@@ -137,9 +137,18 @@ class PleskEmailProvider {
|
|||||||
}
|
}
|
||||||
// stdout sollte die Mail-Infos enthalten
|
// stdout sollte die Mail-Infos enthalten
|
||||||
const exists = result.stdout?.toLowerCase().includes(localPart.toLowerCase());
|
const exists = result.stdout?.toLowerCase().includes(localPart.toLowerCase());
|
||||||
|
// Mailbox-Status aus stdout parsen (Format: "Mailbox: true" oder "Mailbox: false")
|
||||||
|
let hasMailbox;
|
||||||
|
if (exists && result.stdout) {
|
||||||
|
const mailboxMatch = result.stdout.match(/Mailbox:\s*(true|false)/i);
|
||||||
|
if (mailboxMatch) {
|
||||||
|
hasMailbox = mailboxMatch[1].toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
exists,
|
exists,
|
||||||
email: exists ? email : undefined,
|
email: exists ? email : undefined,
|
||||||
|
hasMailbox,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -169,11 +178,12 @@ class PleskEmailProvider {
|
|||||||
}
|
}
|
||||||
// Plesk CLI API: Mail-Account mit Weiterleitung erstellen
|
// Plesk CLI API: Mail-Account mit Weiterleitung erstellen
|
||||||
// Verwendet den CLI-Wrapper unter /api/v2/cli/mail/call
|
// Verwendet den CLI-Wrapper unter /api/v2/cli/mail/call
|
||||||
|
// Format für -forwarding-addresses: "add:email1,email2" oder "set:email1,email2"
|
||||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
params: [
|
params: [
|
||||||
'--create', email,
|
'--create', email,
|
||||||
'-forwarding', 'true',
|
'-forwarding', 'true',
|
||||||
'-forwarding-addresses', forwardTargets.join(','),
|
'-forwarding-addresses', `add:${forwardTargets.join(',')}`,
|
||||||
'-mailbox', 'false',
|
'-mailbox', 'false',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -191,6 +201,118 @@ class PleskEmailProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async createEmailWithMailbox(params) {
|
||||||
|
const { localPart, forwardTargets, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
try {
|
||||||
|
// Prüfen ob schon existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} existiert bereits`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Plesk CLI API: Mail-Account mit echter Mailbox erstellen
|
||||||
|
// -mailbox true: Echte Mailbox (IMAP/SMTP-Zugang)
|
||||||
|
// -passwd: Passwort für die Mailbox
|
||||||
|
// -forwarding true: Zusätzlich Weiterleitung aktivieren
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--create', email,
|
||||||
|
'-mailbox', 'true',
|
||||||
|
'-passwd', password,
|
||||||
|
'-forwarding', 'true',
|
||||||
|
'-forwarding-addresses', `add:${forwardTargets.join(',')}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `E-Mail ${email} mit Mailbox erfolgreich erstellt`,
|
||||||
|
email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk createEmailWithMailbox error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Erstellen der E-Mail mit Mailbox: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async enableMailboxForExisting(params) {
|
||||||
|
const { localPart, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
try {
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (!exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} nicht gefunden`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Plesk CLI API: Mailbox für existierende E-Mail aktivieren
|
||||||
|
// --update: Existierende E-Mail aktualisieren
|
||||||
|
// -mailbox true: Mailbox aktivieren
|
||||||
|
// -passwd: Passwort für die Mailbox setzen
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--update', email,
|
||||||
|
'-mailbox', 'true',
|
||||||
|
'-passwd', password,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Mailbox für ${email} erfolgreich aktiviert`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk enableMailboxForExisting error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Aktivieren der Mailbox: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async updateMailboxPassword(params) {
|
||||||
|
const { localPart, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
try {
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (!exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} nicht gefunden`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Plesk CLI API: Passwort für existierende E-Mail aktualisieren
|
||||||
|
// --update: Existierende E-Mail aktualisieren
|
||||||
|
// -passwd: Neues Passwort setzen
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--update', email,
|
||||||
|
'-passwd', password,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Passwort für ${email} erfolgreich aktualisiert`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk updateMailboxPassword error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Aktualisieren des Passworts: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
async deleteEmail(localPart) {
|
async deleteEmail(localPart) {
|
||||||
const email = `${localPart}@${this.config.domain}`;
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
try {
|
try {
|
||||||
@@ -271,11 +393,12 @@ class PleskEmailProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Plesk CLI API: Weiterleitungsziele aktualisieren
|
// Plesk CLI API: Weiterleitungsziele aktualisieren
|
||||||
|
// Format für -forwarding-addresses: "set:email1,email2" ersetzt alle Adressen
|
||||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
params: [
|
params: [
|
||||||
'--update', email,
|
'--update', email,
|
||||||
'-forwarding', 'true',
|
'-forwarding', 'true',
|
||||||
'-forwarding-addresses', targets.join(','),
|
'-forwarding-addresses', `set:${targets.join(',')}`,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+28
@@ -1,3 +1,4 @@
|
|||||||
|
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
export interface EmailForwardTarget {
|
export interface EmailForwardTarget {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
@@ -5,6 +6,22 @@ export interface CreateEmailParams {
|
|||||||
localPart: string;
|
localPart: string;
|
||||||
forwardTargets: string[];
|
forwardTargets: string[];
|
||||||
}
|
}
|
||||||
|
export interface CreateEmailWithMailboxParams {
|
||||||
|
localPart: string;
|
||||||
|
forwardTargets: string[];
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
export interface CreateEmailWithMailboxResult extends EmailOperationResult {
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
export interface EnableMailboxParams {
|
||||||
|
localPart: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
export interface UpdateMailboxPasswordParams {
|
||||||
|
localPart: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
export interface RenameEmailParams {
|
export interface RenameEmailParams {
|
||||||
oldLocalPart: string;
|
oldLocalPart: string;
|
||||||
newLocalPart: string;
|
newLocalPart: string;
|
||||||
@@ -12,6 +29,7 @@ export interface RenameEmailParams {
|
|||||||
export interface EmailExistsResult {
|
export interface EmailExistsResult {
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
hasMailbox?: boolean;
|
||||||
}
|
}
|
||||||
export interface EmailOperationResult {
|
export interface EmailOperationResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -23,6 +41,9 @@ export interface IEmailProvider {
|
|||||||
testConnection(): Promise<void>;
|
testConnection(): Promise<void>;
|
||||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||||
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
||||||
|
createEmailWithMailbox(params: CreateEmailWithMailboxParams): Promise<CreateEmailWithMailboxResult>;
|
||||||
|
enableMailboxForExisting(params: EnableMailboxParams): Promise<EmailOperationResult>;
|
||||||
|
updateMailboxPassword(params: UpdateMailboxPasswordParams): Promise<EmailOperationResult>;
|
||||||
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
||||||
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
||||||
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
||||||
@@ -37,6 +58,13 @@ export interface EmailProviderConfig {
|
|||||||
password?: string;
|
password?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail?: string;
|
defaultForwardEmail?: string;
|
||||||
|
imapServer?: string;
|
||||||
|
imapPort?: number;
|
||||||
|
smtpServer?: string;
|
||||||
|
smtpPort?: number;
|
||||||
|
imapEncryption?: MailEncryption;
|
||||||
|
smtpEncryption?: MailEncryption;
|
||||||
|
allowSelfSignedCerts?: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAGD,MAAM,WAAW,cAAc;IAE7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAGhC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAG3D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAGtE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAG9D,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAGtE,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC3F;AAGD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB"}
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/emailProvider/types.ts"],"names":[],"mappings":"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"}
|
||||||
+56
-6
@@ -6,10 +6,21 @@ export declare function getEmailsByCustomerId(customerId: number, includeInactiv
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
|
}[]>;
|
||||||
|
export declare function getEmailsWithMailboxByCustomerId(customerId: number): Promise<{
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
notes: string | null;
|
||||||
|
_count: {
|
||||||
|
cachedEmails: number;
|
||||||
|
};
|
||||||
|
hasMailbox: boolean;
|
||||||
}[]>;
|
}[]>;
|
||||||
export declare function getEmailById(id: number): Promise<{
|
export declare function getEmailById(id: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -19,17 +30,14 @@ export declare function getEmailById(id: number): Promise<{
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
} | null>;
|
} | null>;
|
||||||
export declare function createEmail(data: {
|
export declare function getEmailWithMailboxById(id: number): Promise<{
|
||||||
customerId: number;
|
|
||||||
email: string;
|
|
||||||
platform?: string;
|
|
||||||
notes?: string;
|
|
||||||
}): Promise<{
|
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
@@ -37,10 +45,32 @@ export declare function createEmail(data: {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
|
platform: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
|
} | null>;
|
||||||
|
export interface CreateEmailData {
|
||||||
|
customerId: number;
|
||||||
|
email: string;
|
||||||
|
platform?: string;
|
||||||
|
notes?: string;
|
||||||
|
provisionAtProvider?: boolean;
|
||||||
|
createMailbox?: boolean;
|
||||||
|
}
|
||||||
|
export declare function createEmail(data: CreateEmailData): Promise<{
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
customerId: number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
}>;
|
}>;
|
||||||
export declare function updateEmail(id: number, data: {
|
export declare function updateEmail(id: number, data: {
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -55,10 +85,12 @@ export declare function updateEmail(id: number, data: {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
}>;
|
}>;
|
||||||
export declare function deleteEmail(id: number): Promise<{
|
export declare function deleteEmail(id: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
@@ -68,9 +100,27 @@ export declare function deleteEmail(id: number): Promise<{
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
hasMailbox: boolean;
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
isProvisioned: boolean;
|
isProvisioned: boolean;
|
||||||
provisionedAt: Date | null;
|
provisionedAt: Date | null;
|
||||||
provisionError: string | null;
|
provisionError: string | null;
|
||||||
|
emailPasswordEncrypted: string | null;
|
||||||
|
}>;
|
||||||
|
export declare function enableMailbox(id: number): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
export declare function syncMailboxStatus(id: number): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
hasMailbox?: boolean;
|
||||||
|
wasUpdated?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
export declare function getDecryptedPassword(id: number): Promise<string | null>;
|
||||||
|
export declare function resetMailboxPassword(id: number): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
password?: string;
|
||||||
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
//# sourceMappingURL=stressfreiEmail.service.d.ts.map
|
//# sourceMappingURL=stressfreiEmail.service.d.ts.map
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"stressfreiEmail.service.d.ts","sourceRoot":"","sources":["../../src/services/stressfreiEmail.service.ts"],"names":[],"mappings":"AAIA,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,UAAQ;;;;;;;;;;;;KAStF;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;UAI5C;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;GAOA;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;;;;;;;;;;;;GAMF;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;GAE3C"}
|
{"version":3,"file":"stressfreiEmail.service.d.ts","sourceRoot":"","sources":["../../src/services/stressfreiEmail.service.ts"],"names":[],"mappings":"AAcA,wBAAsB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,UAAQ;;;;;;;;;;;;;;KAStF;AAGD,wBAAsB,gCAAgC,CAAC,UAAU,EAAE,MAAM;;;;;;;;KAoBxE;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;UAI5C;AAGD,wBAAsB,uBAAuB,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;UAgBvD;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe;;;;;;;;;;;;;;GAuDtD;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;;;;;;;;;;;;;;GAMF;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;GAE3C;AAGD,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmC7F;AAGD,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAgCD;AAGD,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB7E;AAGD,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiCvH"}
|
||||||
+191
-1
@@ -1,11 +1,20 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.getEmailsByCustomerId = getEmailsByCustomerId;
|
exports.getEmailsByCustomerId = getEmailsByCustomerId;
|
||||||
|
exports.getEmailsWithMailboxByCustomerId = getEmailsWithMailboxByCustomerId;
|
||||||
exports.getEmailById = getEmailById;
|
exports.getEmailById = getEmailById;
|
||||||
|
exports.getEmailWithMailboxById = getEmailWithMailboxById;
|
||||||
exports.createEmail = createEmail;
|
exports.createEmail = createEmail;
|
||||||
exports.updateEmail = updateEmail;
|
exports.updateEmail = updateEmail;
|
||||||
exports.deleteEmail = deleteEmail;
|
exports.deleteEmail = deleteEmail;
|
||||||
|
exports.enableMailbox = enableMailbox;
|
||||||
|
exports.syncMailboxStatus = syncMailboxStatus;
|
||||||
|
exports.getDecryptedPassword = getDecryptedPassword;
|
||||||
|
exports.resetMailboxPassword = resetMailboxPassword;
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
|
const encryption_js_1 = require("../utils/encryption.js");
|
||||||
|
const emailProviderService_js_1 = require("./emailProvider/emailProviderService.js");
|
||||||
|
const passwordGenerator_js_1 = require("../utils/passwordGenerator.js");
|
||||||
const prisma = new client_1.PrismaClient();
|
const prisma = new client_1.PrismaClient();
|
||||||
async function getEmailsByCustomerId(customerId, includeInactive = false) {
|
async function getEmailsByCustomerId(customerId, includeInactive = false) {
|
||||||
const where = { customerId };
|
const where = { customerId };
|
||||||
@@ -17,16 +26,96 @@ async function getEmailsByCustomerId(customerId, includeInactive = false) {
|
|||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Mit Mailbox-Status für E-Mail-Client
|
||||||
|
async function getEmailsWithMailboxByCustomerId(customerId) {
|
||||||
|
return prisma.stressfreiEmail.findMany({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
notes: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
cachedEmails: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { email: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
async function getEmailById(id) {
|
async function getEmailById(id) {
|
||||||
return prisma.stressfreiEmail.findUnique({
|
return prisma.stressfreiEmail.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// E-Mail mit Mailbox-Status laden
|
||||||
|
async function getEmailWithMailboxById(id) {
|
||||||
|
return prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
customerId: true,
|
||||||
|
email: true,
|
||||||
|
platform: true,
|
||||||
|
notes: true,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
async function createEmail(data) {
|
async function createEmail(data) {
|
||||||
|
const { provisionAtProvider, createMailbox, ...emailData } = data;
|
||||||
|
// Falls beim Provider anlegen gewünscht
|
||||||
|
if (provisionAtProvider) {
|
||||||
|
// Kunde laden für Weiterleitung
|
||||||
|
const customer = await prisma.customer.findUnique({
|
||||||
|
where: { id: data.customerId },
|
||||||
|
select: { email: true },
|
||||||
|
});
|
||||||
|
if (!customer?.email) {
|
||||||
|
throw new Error('Kunde hat keine E-Mail-Adresse für Weiterleitung');
|
||||||
|
}
|
||||||
|
// LocalPart extrahieren
|
||||||
|
const localPart = data.email.split('@')[0];
|
||||||
|
if (createMailbox) {
|
||||||
|
// Mit echter Mailbox anlegen
|
||||||
|
const password = (0, passwordGenerator_js_1.generateSecurePassword)();
|
||||||
|
const result = await (0, emailProviderService_js_1.provisionEmailWithMailbox)(localPart, customer.email, password);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Fehler beim Anlegen der Mailbox');
|
||||||
|
}
|
||||||
|
// Passwort verschlüsseln und speichern
|
||||||
|
const passwordEncrypted = (0, encryption_js_1.encrypt)(password);
|
||||||
|
return prisma.stressfreiEmail.create({
|
||||||
|
data: {
|
||||||
|
...emailData,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: passwordEncrypted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Nur Weiterleitung anlegen
|
||||||
|
const result = await (0, emailProviderService_js_1.provisionEmail)(localPart, customer.email);
|
||||||
|
if (!result.success && !result.message?.includes('existiert bereits')) {
|
||||||
|
throw new Error(result.error || 'Fehler beim Anlegen der E-Mail');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return prisma.stressfreiEmail.create({
|
return prisma.stressfreiEmail.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...emailData,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
hasMailbox: createMailbox || false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -39,4 +128,105 @@ async function updateEmail(id, data) {
|
|||||||
async function deleteEmail(id) {
|
async function deleteEmail(id) {
|
||||||
return prisma.stressfreiEmail.delete({ where: { id } });
|
return prisma.stressfreiEmail.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
|
// Mailbox nachträglich aktivieren (für existierende E-Mail-Weiterleitung)
|
||||||
|
async function enableMailbox(id) {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
if (stressfreiEmail.hasMailbox) {
|
||||||
|
return { success: false, error: 'Mailbox ist bereits aktiviert' };
|
||||||
|
}
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
const password = (0, passwordGenerator_js_1.generateSecurePassword)();
|
||||||
|
// Mailbox für existierende E-Mail aktivieren (nicht neu erstellen!)
|
||||||
|
const result = await (0, emailProviderService_js_1.enableMailboxForExistingEmail)(localPart, password);
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, error: result.error || 'Fehler beim Aktivieren der Mailbox' };
|
||||||
|
}
|
||||||
|
// Passwort verschlüsseln und speichern
|
||||||
|
const passwordEncrypted = (0, encryption_js_1.encrypt)(password);
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: passwordEncrypted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
// Mailbox-Status mit Provider synchronisieren
|
||||||
|
async function syncMailboxStatus(id) {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { email: true, hasMailbox: true },
|
||||||
|
});
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
// Provider-Status prüfen
|
||||||
|
const providerStatus = await (0, emailProviderService_js_1.checkEmailExists)(localPart);
|
||||||
|
if (!providerStatus.exists) {
|
||||||
|
return { success: true, hasMailbox: false, wasUpdated: false };
|
||||||
|
}
|
||||||
|
const providerHasMailbox = providerStatus.hasMailbox === true;
|
||||||
|
// DB aktualisieren wenn Status abweicht
|
||||||
|
if (stressfreiEmail.hasMailbox !== providerHasMailbox) {
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { hasMailbox: providerHasMailbox },
|
||||||
|
});
|
||||||
|
console.log(`Mailbox-Status für ${stressfreiEmail.email} aktualisiert: ${stressfreiEmail.hasMailbox} -> ${providerHasMailbox}`);
|
||||||
|
return { success: true, hasMailbox: providerHasMailbox, wasUpdated: true };
|
||||||
|
}
|
||||||
|
return { success: true, hasMailbox: providerHasMailbox, wasUpdated: false };
|
||||||
|
}
|
||||||
|
// Passwort für IMAP/SMTP-Zugang entschlüsseln (nur für autorisierte Nutzung)
|
||||||
|
async function getDecryptedPassword(id) {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { emailPasswordEncrypted: true },
|
||||||
|
});
|
||||||
|
if (!stressfreiEmail?.emailPasswordEncrypted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (0, encryption_js_1.decrypt)(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
console.error('Fehler beim Entschlüsseln des Passworts');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Passwort neu generieren und beim Provider setzen
|
||||||
|
async function resetMailboxPassword(id) {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { email: true, hasMailbox: true },
|
||||||
|
});
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
if (!stressfreiEmail.hasMailbox) {
|
||||||
|
return { success: false, error: 'Keine Mailbox für diese E-Mail-Adresse' };
|
||||||
|
}
|
||||||
|
// Neues Passwort generieren
|
||||||
|
const newPassword = (0, passwordGenerator_js_1.generateSecurePassword)();
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
// Passwort beim Provider ändern
|
||||||
|
const providerResult = await (0, emailProviderService_js_1.updateMailboxPassword)(localPart, newPassword);
|
||||||
|
if (!providerResult.success) {
|
||||||
|
return { success: false, error: providerResult.error || 'Fehler beim Aktualisieren des Passworts beim Provider' };
|
||||||
|
}
|
||||||
|
// Passwort verschlüsseln und lokal speichern
|
||||||
|
const passwordEncrypted = (0, encryption_js_1.encrypt)(newPassword);
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { emailPasswordEncrypted: passwordEncrypted },
|
||||||
|
});
|
||||||
|
return { success: true, password: newPassword };
|
||||||
|
}
|
||||||
//# sourceMappingURL=stressfreiEmail.service.js.map
|
//# sourceMappingURL=stressfreiEmail.service.js.map
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+2
@@ -71,6 +71,7 @@ export declare function createUser(data: {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
roleIds: number[];
|
roleIds: number[];
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
|
hasDeveloperAccess?: boolean;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -137,6 +138,7 @@ export declare function deleteUser(id: number): Promise<{
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
tokenInvalidatedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../src/services/user.service.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqErD;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA0C3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;;;;;;;;;;;;;;;;;;;GA0BA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAsHF;AA0DD,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;GA6D1C;AAGD,wBAAsB,WAAW;;;;;;;;;;;;;;;;;;;;MAYhC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;WAS3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;;;;;;;;;;;;;;;;;GAeA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;;;;;;;;;;;;;;;;;WAiBF;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;GAU1C;AAGD,wBAAsB,iBAAiB;;;;KAItC"}
|
{"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../src/services/user.service.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqErD;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA0C3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;;;;;;;;;;;;;;;;;;;GAiCA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAwIF;AAoED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GA6D1C;AAGD,wBAAsB,WAAW;;;;;;;;;;;;;;;;;;;;MAYhC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;WAS3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;;;;;;;;;;;;;;;;;GAeA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;;;;;;;;;;;;;;;;;WAiBF;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;GAU1C;AAGD,wBAAsB,iBAAiB;;;;KAItC"}
|
||||||
+35
-3
@@ -122,7 +122,7 @@ async function getUserById(id) {
|
|||||||
}
|
}
|
||||||
async function createUser(data) {
|
async function createUser(data) {
|
||||||
const hashedPassword = await bcryptjs_1.default.hash(data.password, 10);
|
const hashedPassword = await bcryptjs_1.default.hash(data.password, 10);
|
||||||
return prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
@@ -145,6 +145,11 @@ async function createUser(data) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Entwicklerzugriff setzen falls aktiviert
|
||||||
|
if (data.hasDeveloperAccess) {
|
||||||
|
await setUserDeveloperAccess(user.id, true);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
async function updateUser(id, data) {
|
async function updateUser(id, data) {
|
||||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||||
@@ -224,10 +229,27 @@ async function updateUser(id, data) {
|
|||||||
if (password) {
|
if (password) {
|
||||||
userData.password = await bcryptjs_1.default.hash(password, 10);
|
userData.password = await bcryptjs_1.default.hash(password, 10);
|
||||||
}
|
}
|
||||||
// Update user
|
// Prüfen ob Rollen geändert werden (für Zwangslogout)
|
||||||
|
let rolesChanged = false;
|
||||||
|
if (roleIds !== undefined) {
|
||||||
|
const currentRoles = await prisma.userRole.findMany({
|
||||||
|
where: { userId: id },
|
||||||
|
select: { roleId: true },
|
||||||
|
});
|
||||||
|
const currentRoleIds = currentRoles.map((r) => r.roleId).sort();
|
||||||
|
const newRoleIds = [...roleIds].sort();
|
||||||
|
rolesChanged =
|
||||||
|
currentRoleIds.length !== newRoleIds.length ||
|
||||||
|
!currentRoleIds.every((id, i) => id === newRoleIds[i]);
|
||||||
|
}
|
||||||
|
// Update user - bei Rollenänderung Token invalidieren
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: userData,
|
data: {
|
||||||
|
...userData,
|
||||||
|
// Token invalidieren wenn Rollen geändert werden
|
||||||
|
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// Update roles if provided
|
// Update roles if provided
|
||||||
if (roleIds) {
|
if (roleIds) {
|
||||||
@@ -281,6 +303,11 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||||||
await prisma.userRole.create({
|
await prisma.userRole.create({
|
||||||
data: { userId, roleId: developerRole.id },
|
data: { userId, roleId: developerRole.id },
|
||||||
});
|
});
|
||||||
|
// Token invalidieren bei Rechteänderung
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { tokenInvalidatedAt: new Date() },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (!enabled && hasRole) {
|
else if (!enabled && hasRole) {
|
||||||
// Remove Developer role
|
// Remove Developer role
|
||||||
@@ -288,6 +315,11 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||||||
await prisma.userRole.delete({
|
await prisma.userRole.delete({
|
||||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||||
});
|
});
|
||||||
|
// Token invalidieren bei Rechteänderung
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { tokenInvalidatedAt: new Date() },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -6,6 +6,7 @@ export interface JwtPayload {
|
|||||||
customerId?: number;
|
customerId?: number;
|
||||||
isCustomerPortal?: boolean;
|
isCustomerPortal?: boolean;
|
||||||
representedCustomerIds?: number[];
|
representedCustomerIds?: number[];
|
||||||
|
iat?: number;
|
||||||
}
|
}
|
||||||
export interface AuthRequest extends Request {
|
export interface AuthRequest extends Request {
|
||||||
user?: JwtPayload;
|
user?: JwtPayload;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH"}
|
||||||
+1387
File diff suppressed because it is too large
Load Diff
+53
-4
File diff suppressed because one or more lines are too long
+50
@@ -137,6 +137,7 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||||||
firstName: 'firstName',
|
firstName: 'firstName',
|
||||||
lastName: 'lastName',
|
lastName: 'lastName',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
|
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||||
customerId: 'customerId',
|
customerId: 'customerId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
@@ -259,6 +260,13 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||||||
passwordEncrypted: 'passwordEncrypted',
|
passwordEncrypted: 'passwordEncrypted',
|
||||||
domain: 'domain',
|
domain: 'domain',
|
||||||
defaultForwardEmail: 'defaultForwardEmail',
|
defaultForwardEmail: 'defaultForwardEmail',
|
||||||
|
imapServer: 'imapServer',
|
||||||
|
imapPort: 'imapPort',
|
||||||
|
smtpServer: 'smtpServer',
|
||||||
|
smtpPort: 'smtpPort',
|
||||||
|
imapEncryption: 'imapEncryption',
|
||||||
|
smtpEncryption: 'smtpEncryption',
|
||||||
|
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
isDefault: 'isDefault',
|
isDefault: 'isDefault',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -275,6 +283,36 @@ exports.Prisma.StressfreiEmailScalarFieldEnum = {
|
|||||||
isProvisioned: 'isProvisioned',
|
isProvisioned: 'isProvisioned',
|
||||||
provisionedAt: 'provisionedAt',
|
provisionedAt: 'provisionedAt',
|
||||||
provisionError: 'provisionError',
|
provisionError: 'provisionError',
|
||||||
|
hasMailbox: 'hasMailbox',
|
||||||
|
emailPasswordEncrypted: 'emailPasswordEncrypted',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.CachedEmailScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
stressfreiEmailId: 'stressfreiEmailId',
|
||||||
|
folder: 'folder',
|
||||||
|
messageId: 'messageId',
|
||||||
|
uid: 'uid',
|
||||||
|
subject: 'subject',
|
||||||
|
fromAddress: 'fromAddress',
|
||||||
|
fromName: 'fromName',
|
||||||
|
toAddresses: 'toAddresses',
|
||||||
|
ccAddresses: 'ccAddresses',
|
||||||
|
receivedAt: 'receivedAt',
|
||||||
|
textBody: 'textBody',
|
||||||
|
htmlBody: 'htmlBody',
|
||||||
|
hasAttachments: 'hasAttachments',
|
||||||
|
attachmentNames: 'attachmentNames',
|
||||||
|
contractId: 'contractId',
|
||||||
|
assignedAt: 'assignedAt',
|
||||||
|
assignedBy: 'assignedBy',
|
||||||
|
isAutoAssigned: 'isAutoAssigned',
|
||||||
|
isRead: 'isRead',
|
||||||
|
isStarred: 'isStarred',
|
||||||
|
isDeleted: 'isDeleted',
|
||||||
|
deletedAt: 'deletedAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -542,6 +580,17 @@ exports.EmailProviderType = exports.$Enums.EmailProviderType = {
|
|||||||
DIRECTADMIN: 'DIRECTADMIN'
|
DIRECTADMIN: 'DIRECTADMIN'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.MailEncryption = exports.$Enums.MailEncryption = {
|
||||||
|
SSL: 'SSL',
|
||||||
|
STARTTLS: 'STARTTLS',
|
||||||
|
NONE: 'NONE'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.EmailFolder = exports.$Enums.EmailFolder = {
|
||||||
|
INBOX: 'INBOX',
|
||||||
|
SENT: 'SENT'
|
||||||
|
};
|
||||||
|
|
||||||
exports.MeterType = exports.$Enums.MeterType = {
|
exports.MeterType = exports.$Enums.MeterType = {
|
||||||
ELECTRICITY: 'ELECTRICITY',
|
ELECTRICITY: 'ELECTRICITY',
|
||||||
GAS: 'GAS'
|
GAS: 'GAS'
|
||||||
@@ -592,6 +641,7 @@ exports.Prisma.ModelName = {
|
|||||||
IdentityDocument: 'IdentityDocument',
|
IdentityDocument: 'IdentityDocument',
|
||||||
EmailProviderConfig: 'EmailProviderConfig',
|
EmailProviderConfig: 'EmailProviderConfig',
|
||||||
StressfreiEmail: 'StressfreiEmail',
|
StressfreiEmail: 'StressfreiEmail',
|
||||||
|
CachedEmail: 'CachedEmail',
|
||||||
Meter: 'Meter',
|
Meter: 'Meter',
|
||||||
MeterReading: 'MeterReading',
|
MeterReading: 'MeterReading',
|
||||||
SalesPlatform: 'SalesPlatform',
|
SalesPlatform: 'SalesPlatform',
|
||||||
|
|||||||
+3210
-60
File diff suppressed because it is too large
Load Diff
+53
-4
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-f3be941c86c0d933a2a09d69aafc49ad121411869df4ce4f365fdf53679b90db",
|
"name": "prisma-client-3c4bb688688ba372393d0bf86523c07e8b4de3ff0d9ad23a89f905f15047a1a5",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|||||||
+109
-25
@@ -20,17 +20,18 @@ model AppSetting {
|
|||||||
// ==================== USERS & AUTH ====================
|
// ==================== USERS & AUTH ====================
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
firstName String
|
firstName String
|
||||||
lastName String
|
lastName String
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
customerId Int? @unique
|
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||||
customer Customer? @relation(fields: [customerId], references: [id])
|
customerId Int? @unique
|
||||||
roles UserRole[]
|
customer Customer? @relation(fields: [customerId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
roles UserRole[]
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
@@ -216,6 +217,13 @@ enum EmailProviderType {
|
|||||||
DIRECTADMIN
|
DIRECTADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verschlüsselungstyp für E-Mail-Verbindungen
|
||||||
|
enum MailEncryption {
|
||||||
|
SSL // Implicit SSL/TLS (Ports 465/993) - Verschlüsselung von Anfang an
|
||||||
|
STARTTLS // STARTTLS (Ports 587/143) - Startet unverschlüsselt, dann Upgrade
|
||||||
|
NONE // Keine Verschlüsselung (Ports 25/143)
|
||||||
|
}
|
||||||
|
|
||||||
model EmailProviderConfig {
|
model EmailProviderConfig {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique // z.B. "Plesk Hauptserver"
|
name String @unique // z.B. "Plesk Hauptserver"
|
||||||
@@ -226,28 +234,103 @@ model EmailProviderConfig {
|
|||||||
passwordEncrypted String? // Passwort (verschlüsselt)
|
passwordEncrypted String? // Passwort (verschlüsselt)
|
||||||
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
||||||
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
||||||
isActive Boolean @default(true)
|
|
||||||
isDefault Boolean @default(false) // Standard-Provider
|
// IMAP/SMTP-Server für E-Mail-Client (optional, default: mail.{domain})
|
||||||
createdAt DateTime @default(now())
|
imapServer String? // z.B. "mail.stressfrei-wechseln.de"
|
||||||
updatedAt DateTime @updatedAt
|
imapPort Int? @default(993)
|
||||||
|
smtpServer String?
|
||||||
|
smtpPort Int? @default(465)
|
||||||
|
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
imapEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||||
|
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||||
|
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
|
||||||
|
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
isDefault Boolean @default(false) // Standard-Provider
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
||||||
|
|
||||||
model StressfreiEmail {
|
model StressfreiEmail {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customerId Int
|
customerId Int
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||||
notes String? @db.Text // Optionale Notizen
|
notes String? @db.Text // Optionale Notizen
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
|
||||||
createdAt DateTime @default(now())
|
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
|
||||||
updatedAt DateTime @updatedAt
|
hasMailbox Boolean @default(false) // Hat echte Mailbox (nicht nur Weiterleitung)?
|
||||||
|
emailPasswordEncrypted String? // Verschlüsseltes Mailbox-Passwort (AES-256-GCM)
|
||||||
|
|
||||||
|
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||||
|
cachedEmails CachedEmail[] // Gecachte E-Mails aus dieser Mailbox
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CACHED EMAILS (E-Mail-Client) ====================
|
||||||
|
|
||||||
|
enum EmailFolder {
|
||||||
|
INBOX
|
||||||
|
SENT
|
||||||
|
}
|
||||||
|
|
||||||
|
model CachedEmail {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
stressfreiEmailId Int
|
||||||
|
stressfreiEmail StressfreiEmail @relation(fields: [stressfreiEmailId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
// Ordner (Posteingang oder Gesendet)
|
||||||
|
folder EmailFolder @default(INBOX)
|
||||||
|
|
||||||
|
// IMAP-Identifikation
|
||||||
|
messageId String // RFC 5322 Message-ID
|
||||||
|
uid Int // IMAP UID (für Synchronisierung, bei SENT = 0)
|
||||||
|
|
||||||
|
// E-Mail-Metadaten
|
||||||
|
subject String?
|
||||||
|
fromAddress String
|
||||||
|
fromName String?
|
||||||
|
toAddresses String @db.Text // JSON Array
|
||||||
|
ccAddresses String? @db.Text // JSON Array
|
||||||
|
receivedAt DateTime
|
||||||
|
|
||||||
|
// Inhalt
|
||||||
|
textBody String? @db.LongText
|
||||||
|
htmlBody String? @db.LongText
|
||||||
|
hasAttachments Boolean @default(false)
|
||||||
|
attachmentNames String? @db.Text // JSON Array
|
||||||
|
|
||||||
|
// Vertragszuordnung
|
||||||
|
contractId Int?
|
||||||
|
contract Contract? @relation(fields: [contractId], references: [id], onDelete: SetNull)
|
||||||
|
assignedAt DateTime?
|
||||||
|
assignedBy Int? // User ID der die Zuordnung gemacht hat
|
||||||
|
isAutoAssigned Boolean @default(false) // true = automatisch beim Senden aus Vertrag
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
isRead Boolean @default(false)
|
||||||
|
isStarred Boolean @default(false)
|
||||||
|
|
||||||
|
// Papierkorb
|
||||||
|
isDeleted Boolean @default(false) // Im Papierkorb?
|
||||||
|
deletedAt DateTime? // Wann gelöscht?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([stressfreiEmailId, messageId, folder]) // Folder hinzugefügt: gleiche MessageID kann in INBOX und SENT existieren
|
||||||
|
@@index([contractId])
|
||||||
|
@@index([stressfreiEmailId, folder, receivedAt])
|
||||||
|
@@index([stressfreiEmailId, isDeleted]) // Für Papierkorb-Abfragen
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== METERS (Energy) ====================
|
// ==================== METERS (Energy) ====================
|
||||||
@@ -464,7 +547,8 @@ model Contract {
|
|||||||
tvDetails TvContractDetails?
|
tvDetails TvContractDetails?
|
||||||
carInsuranceDetails CarInsuranceDetails?
|
carInsuranceDetails CarInsuranceDetails?
|
||||||
|
|
||||||
tasks ContractTask[]
|
tasks ContractTask[]
|
||||||
|
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
+50
@@ -137,6 +137,7 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||||||
firstName: 'firstName',
|
firstName: 'firstName',
|
||||||
lastName: 'lastName',
|
lastName: 'lastName',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
|
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||||
customerId: 'customerId',
|
customerId: 'customerId',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
@@ -259,6 +260,13 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||||||
passwordEncrypted: 'passwordEncrypted',
|
passwordEncrypted: 'passwordEncrypted',
|
||||||
domain: 'domain',
|
domain: 'domain',
|
||||||
defaultForwardEmail: 'defaultForwardEmail',
|
defaultForwardEmail: 'defaultForwardEmail',
|
||||||
|
imapServer: 'imapServer',
|
||||||
|
imapPort: 'imapPort',
|
||||||
|
smtpServer: 'smtpServer',
|
||||||
|
smtpPort: 'smtpPort',
|
||||||
|
imapEncryption: 'imapEncryption',
|
||||||
|
smtpEncryption: 'smtpEncryption',
|
||||||
|
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
isDefault: 'isDefault',
|
isDefault: 'isDefault',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
@@ -275,6 +283,36 @@ exports.Prisma.StressfreiEmailScalarFieldEnum = {
|
|||||||
isProvisioned: 'isProvisioned',
|
isProvisioned: 'isProvisioned',
|
||||||
provisionedAt: 'provisionedAt',
|
provisionedAt: 'provisionedAt',
|
||||||
provisionError: 'provisionError',
|
provisionError: 'provisionError',
|
||||||
|
hasMailbox: 'hasMailbox',
|
||||||
|
emailPasswordEncrypted: 'emailPasswordEncrypted',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.CachedEmailScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
stressfreiEmailId: 'stressfreiEmailId',
|
||||||
|
folder: 'folder',
|
||||||
|
messageId: 'messageId',
|
||||||
|
uid: 'uid',
|
||||||
|
subject: 'subject',
|
||||||
|
fromAddress: 'fromAddress',
|
||||||
|
fromName: 'fromName',
|
||||||
|
toAddresses: 'toAddresses',
|
||||||
|
ccAddresses: 'ccAddresses',
|
||||||
|
receivedAt: 'receivedAt',
|
||||||
|
textBody: 'textBody',
|
||||||
|
htmlBody: 'htmlBody',
|
||||||
|
hasAttachments: 'hasAttachments',
|
||||||
|
attachmentNames: 'attachmentNames',
|
||||||
|
contractId: 'contractId',
|
||||||
|
assignedAt: 'assignedAt',
|
||||||
|
assignedBy: 'assignedBy',
|
||||||
|
isAutoAssigned: 'isAutoAssigned',
|
||||||
|
isRead: 'isRead',
|
||||||
|
isStarred: 'isStarred',
|
||||||
|
isDeleted: 'isDeleted',
|
||||||
|
deletedAt: 'deletedAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -542,6 +580,17 @@ exports.EmailProviderType = exports.$Enums.EmailProviderType = {
|
|||||||
DIRECTADMIN: 'DIRECTADMIN'
|
DIRECTADMIN: 'DIRECTADMIN'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.MailEncryption = exports.$Enums.MailEncryption = {
|
||||||
|
SSL: 'SSL',
|
||||||
|
STARTTLS: 'STARTTLS',
|
||||||
|
NONE: 'NONE'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.EmailFolder = exports.$Enums.EmailFolder = {
|
||||||
|
INBOX: 'INBOX',
|
||||||
|
SENT: 'SENT'
|
||||||
|
};
|
||||||
|
|
||||||
exports.MeterType = exports.$Enums.MeterType = {
|
exports.MeterType = exports.$Enums.MeterType = {
|
||||||
ELECTRICITY: 'ELECTRICITY',
|
ELECTRICITY: 'ELECTRICITY',
|
||||||
GAS: 'GAS'
|
GAS: 'GAS'
|
||||||
@@ -592,6 +641,7 @@ exports.Prisma.ModelName = {
|
|||||||
IdentityDocument: 'IdentityDocument',
|
IdentityDocument: 'IdentityDocument',
|
||||||
EmailProviderConfig: 'EmailProviderConfig',
|
EmailProviderConfig: 'EmailProviderConfig',
|
||||||
StressfreiEmail: 'StressfreiEmail',
|
StressfreiEmail: 'StressfreiEmail',
|
||||||
|
CachedEmail: 'CachedEmail',
|
||||||
Meter: 'Meter',
|
Meter: 'Meter',
|
||||||
MeterReading: 'MeterReading',
|
MeterReading: 'MeterReading',
|
||||||
SalesPlatform: 'SalesPlatform',
|
SalesPlatform: 'SalesPlatform',
|
||||||
|
|||||||
Generated
+1396
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -13,26 +13,37 @@
|
|||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:seed": "tsx prisma/seed.ts",
|
"db:seed": "tsx prisma/seed.ts",
|
||||||
"db:studio": "prisma studio"
|
"db:studio": "prisma studio",
|
||||||
|
"db:backup": "tsx prisma/backup-data.ts",
|
||||||
|
"db:restore": "tsx prisma/restore-data.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"express-validator": "^7.2.0",
|
"express-validator": "^7.2.0",
|
||||||
|
"imapflow": "^1.2.8",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mailparser": "^3.9.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemailer": "^7.0.13",
|
||||||
"undici": "^6.23.0"
|
"undici": "^6.23.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
|
"@types/archiver": "^7.0.0",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.25",
|
"@types/express": "^4.17.25",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/mailparser": "^3.4.6",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
|
"@types/nodemailer": "^7.0.9",
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Adding/updating permissions and Developer role...');
|
||||||
|
|
||||||
|
// 1. Create or get the emails:delete permission
|
||||||
|
const emailsDeletePerm = await prisma.permission.upsert({
|
||||||
|
where: { resource_action: { resource: 'emails', action: 'delete' } },
|
||||||
|
update: {},
|
||||||
|
create: { resource: 'emails', action: 'delete' },
|
||||||
|
});
|
||||||
|
console.log('emails:delete permission created/found');
|
||||||
|
|
||||||
|
// 2. Create or get the developer:access permission
|
||||||
|
const developerAccessPerm = await prisma.permission.upsert({
|
||||||
|
where: { resource_action: { resource: 'developer', action: 'access' } },
|
||||||
|
update: {},
|
||||||
|
create: { resource: 'developer', action: 'access' },
|
||||||
|
});
|
||||||
|
console.log('developer:access permission created/found');
|
||||||
|
|
||||||
|
// 3. Create Developer role if it doesn't exist
|
||||||
|
let developerRole = await prisma.role.findUnique({
|
||||||
|
where: { name: 'Developer' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!developerRole) {
|
||||||
|
// Get all permissions for Developer role
|
||||||
|
const allPermissions = await prisma.permission.findMany();
|
||||||
|
developerRole = await prisma.role.create({
|
||||||
|
data: {
|
||||||
|
name: 'Developer',
|
||||||
|
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||||||
|
permissions: {
|
||||||
|
create: allPermissions.map(p => ({ permissionId: p.id })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Developer role created with all permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Add emails:delete to Admin and Developer
|
||||||
|
const rolesToUpdate = [
|
||||||
|
{ name: 'Admin', permissions: [emailsDeletePerm] },
|
||||||
|
{ name: 'Developer', permissions: [emailsDeletePerm, developerAccessPerm] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const roleConfig of rolesToUpdate) {
|
||||||
|
const role = await prisma.role.findUnique({
|
||||||
|
where: { name: roleConfig.name },
|
||||||
|
include: { permissions: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
console.log(`${roleConfig.name} role not found, skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const perm of roleConfig.permissions) {
|
||||||
|
const hasPermission = role.permissions.some(
|
||||||
|
(rp) => rp.permissionId === perm.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
await prisma.rolePermission.create({
|
||||||
|
data: {
|
||||||
|
roleId: role.id,
|
||||||
|
permissionId: perm.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Added ${perm.resource}:${perm.action} to ${roleConfig.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Datenbank-Backup Script
|
||||||
|
*
|
||||||
|
* Exportiert alle Daten als JSON-Dateien für die Wiederherstellung nach Migrationen.
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* npx ts-node prisma/backup-data.ts
|
||||||
|
*
|
||||||
|
* Erstellt einen Ordner 'backups/YYYY-MM-DD_HH-mm-ss/' mit JSON-Dateien pro Tabelle.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Backup-Ordner mit Zeitstempel erstellen
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
|
const backupDir = path.join(__dirname, 'backups', timestamp);
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupDir)) {
|
||||||
|
fs.mkdirSync(backupDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📦 Starte Datenbank-Backup nach: ${backupDir}\n`);
|
||||||
|
|
||||||
|
// Tabellen in Abhängigkeitsreihenfolge (unabhängige zuerst)
|
||||||
|
const tables = [
|
||||||
|
// Level 0: Keine Abhängigkeiten
|
||||||
|
{ name: 'Permission', query: () => prisma.permission.findMany() },
|
||||||
|
{ name: 'Role', query: () => prisma.role.findMany() },
|
||||||
|
{ name: 'SalesPlatform', query: () => prisma.salesPlatform.findMany() },
|
||||||
|
{ name: 'ContractCategory', query: () => prisma.contractCategory.findMany() },
|
||||||
|
{ name: 'CancellationPeriod', query: () => prisma.cancellationPeriod.findMany() },
|
||||||
|
{ name: 'ContractDuration', query: () => prisma.contractDuration.findMany() },
|
||||||
|
{ name: 'AppSetting', query: () => prisma.appSetting.findMany() },
|
||||||
|
{ name: 'EmailProviderConfig', query: () => prisma.emailProviderConfig.findMany() },
|
||||||
|
{ name: 'EnergyProvider', query: () => prisma.energyProvider.findMany() },
|
||||||
|
{ name: 'TelecomProvider', query: () => prisma.telecomProvider.findMany() },
|
||||||
|
|
||||||
|
// Level 1: Abhängig von Level 0
|
||||||
|
{ name: 'RolePermission', query: () => prisma.rolePermission.findMany() },
|
||||||
|
{ name: 'User', query: () => prisma.user.findMany() },
|
||||||
|
{ name: 'Customer', query: () => prisma.customer.findMany() },
|
||||||
|
{ name: 'Tariff', query: () => prisma.tariff.findMany() },
|
||||||
|
|
||||||
|
// Level 2: Abhängig von Level 1
|
||||||
|
{ name: 'UserRole', query: () => prisma.userRole.findMany() },
|
||||||
|
{ name: 'CustomerRepresentative', query: () => prisma.customerRepresentative.findMany() },
|
||||||
|
{ name: 'StressfreiEmail', query: () => prisma.stressfreiEmail.findMany() },
|
||||||
|
{ name: 'Contract', query: () => prisma.contract.findMany() },
|
||||||
|
{ name: 'Meter', query: () => prisma.meter.findMany() },
|
||||||
|
|
||||||
|
// Level 3: Abhängig von Level 2
|
||||||
|
{ name: 'CachedEmail', query: () => prisma.cachedEmail.findMany() },
|
||||||
|
{ name: 'ContractTask', query: () => prisma.contractTask.findMany() },
|
||||||
|
{ name: 'MeterReading', query: () => prisma.meterReading.findMany() },
|
||||||
|
{ name: 'ContractNote', query: () => prisma.contractNote.findMany() },
|
||||||
|
{ name: 'ContractDocument', query: () => prisma.contractDocument.findMany() },
|
||||||
|
|
||||||
|
// Level 4: Abhängig von Level 3
|
||||||
|
{ name: 'ContractTaskSubtask', query: () => prisma.contractTaskSubtask.findMany() },
|
||||||
|
|
||||||
|
// Vertragstyp-spezifische Details
|
||||||
|
{ name: 'EnergyContractDetails', query: () => prisma.energyContractDetails.findMany() },
|
||||||
|
{ name: 'TelecomContractDetails', query: () => prisma.telecomContractDetails.findMany() },
|
||||||
|
{ name: 'CarInsuranceDetails', query: () => prisma.carInsuranceDetails.findMany() },
|
||||||
|
];
|
||||||
|
|
||||||
|
let totalRecords = 0;
|
||||||
|
const stats: { table: string; count: number }[] = [];
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
try {
|
||||||
|
const data = await table.query();
|
||||||
|
const count = data.length;
|
||||||
|
totalRecords += count;
|
||||||
|
stats.push({ table: table.name, count });
|
||||||
|
|
||||||
|
// JSON-Datei schreiben
|
||||||
|
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
const status = count > 0 ? '✅' : '⚪';
|
||||||
|
console.log(`${status} ${table.name}: ${count} Einträge`);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Tabelle existiert möglicherweise nicht (bei älteren Schema-Versionen)
|
||||||
|
console.log(`⚠️ ${table.name}: Übersprungen (${error.message?.slice(0, 50)}...)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup-Info speichern
|
||||||
|
const backupInfo = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
totalRecords,
|
||||||
|
tables: stats,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(path.join(backupDir, '_backup-info.json'), JSON.stringify(backupInfo, null, 2));
|
||||||
|
|
||||||
|
console.log(`\n✅ Backup abgeschlossen!`);
|
||||||
|
console.log(` 📊 ${totalRecords} Datensätze in ${stats.filter(s => s.count > 0).length} Tabellen`);
|
||||||
|
console.log(` 📁 Gespeichert in: ${backupDir}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('❌ Backup fehlgeschlagen:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[portalEmail]` on the table `Customer` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Contract` ADD COLUMN `stressfreiEmailId` INTEGER NULL,
|
||||||
|
MODIFY `status` ENUM('DRAFT', 'PENDING', 'ACTIVE', 'CANCELLED', 'EXPIRED', 'DEACTIVATED') NOT NULL DEFAULT 'DRAFT';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Customer` ADD COLUMN `portalEmail` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `portalEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN `portalLastLogin` DATETIME(3) NULL,
|
||||||
|
ADD COLUMN `portalPasswordEncrypted` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `portalPasswordHash` VARCHAR(191) NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `AppSetting` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`key` VARCHAR(191) NOT NULL,
|
||||||
|
`value` TEXT NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `AppSetting_key_key`(`key`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CustomerRepresentative` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`customerId` INTEGER NOT NULL,
|
||||||
|
`representativeId` INTEGER NOT NULL,
|
||||||
|
`notes` VARCHAR(191) NULL,
|
||||||
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `CustomerRepresentative_customerId_representativeId_key`(`customerId`, `representativeId`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `EmailProviderConfig` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`type` ENUM('PLESK', 'CPANEL', 'DIRECTADMIN') NOT NULL,
|
||||||
|
`apiUrl` VARCHAR(191) NOT NULL,
|
||||||
|
`apiKey` VARCHAR(191) NULL,
|
||||||
|
`username` VARCHAR(191) NULL,
|
||||||
|
`passwordEncrypted` VARCHAR(191) NULL,
|
||||||
|
`domain` VARCHAR(191) NOT NULL,
|
||||||
|
`defaultForwardEmail` VARCHAR(191) NULL,
|
||||||
|
`imapServer` VARCHAR(191) NULL,
|
||||||
|
`imapPort` INTEGER NULL DEFAULT 993,
|
||||||
|
`smtpServer` VARCHAR(191) NULL,
|
||||||
|
`smtpPort` INTEGER NULL DEFAULT 465,
|
||||||
|
`imapEncryption` ENUM('SSL', 'STARTTLS', 'NONE') NOT NULL DEFAULT 'SSL',
|
||||||
|
`smtpEncryption` ENUM('SSL', 'STARTTLS', 'NONE') NOT NULL DEFAULT 'SSL',
|
||||||
|
`allowSelfSignedCerts` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`isDefault` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `EmailProviderConfig_name_key`(`name`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `StressfreiEmail` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`customerId` INTEGER NOT NULL,
|
||||||
|
`email` VARCHAR(191) NOT NULL,
|
||||||
|
`platform` VARCHAR(191) NULL,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`isProvisioned` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`provisionedAt` DATETIME(3) NULL,
|
||||||
|
`provisionError` TEXT NULL,
|
||||||
|
`hasMailbox` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`emailPasswordEncrypted` VARCHAR(191) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `CachedEmail` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`stressfreiEmailId` INTEGER NOT NULL,
|
||||||
|
`folder` ENUM('INBOX', 'SENT') NOT NULL DEFAULT 'INBOX',
|
||||||
|
`messageId` VARCHAR(191) NOT NULL,
|
||||||
|
`uid` INTEGER NOT NULL,
|
||||||
|
`subject` VARCHAR(191) NULL,
|
||||||
|
`fromAddress` VARCHAR(191) NOT NULL,
|
||||||
|
`fromName` VARCHAR(191) NULL,
|
||||||
|
`toAddresses` TEXT NOT NULL,
|
||||||
|
`ccAddresses` TEXT NULL,
|
||||||
|
`receivedAt` DATETIME(3) NOT NULL,
|
||||||
|
`textBody` LONGTEXT NULL,
|
||||||
|
`htmlBody` LONGTEXT NULL,
|
||||||
|
`hasAttachments` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`attachmentNames` TEXT NULL,
|
||||||
|
`contractId` INTEGER NULL,
|
||||||
|
`assignedAt` DATETIME(3) NULL,
|
||||||
|
`assignedBy` INTEGER NULL,
|
||||||
|
`isAutoAssigned` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`isRead` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`isStarred` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`isDeleted` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`deletedAt` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
INDEX `CachedEmail_contractId_idx`(`contractId`),
|
||||||
|
INDEX `CachedEmail_stressfreiEmailId_folder_receivedAt_idx`(`stressfreiEmailId`, `folder`, `receivedAt`),
|
||||||
|
INDEX `CachedEmail_stressfreiEmailId_isDeleted_idx`(`stressfreiEmailId`, `isDeleted`),
|
||||||
|
UNIQUE INDEX `CachedEmail_stressfreiEmailId_messageId_folder_key`(`stressfreiEmailId`, `messageId`, `folder`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `ContractTask` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`contractId` INTEGER NOT NULL,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`description` TEXT NULL,
|
||||||
|
`status` ENUM('OPEN', 'COMPLETED') NOT NULL DEFAULT 'OPEN',
|
||||||
|
`visibleInPortal` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`createdBy` VARCHAR(191) NULL,
|
||||||
|
`completedAt` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `ContractTaskSubtask` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`taskId` INTEGER NOT NULL,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`status` ENUM('OPEN', 'COMPLETED') NOT NULL DEFAULT 'OPEN',
|
||||||
|
`createdBy` VARCHAR(191) NULL,
|
||||||
|
`completedAt` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `Customer_portalEmail_key` ON `Customer`(`portalEmail`);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CustomerRepresentative` ADD CONSTRAINT `CustomerRepresentative_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CustomerRepresentative` ADD CONSTRAINT `CustomerRepresentative_representativeId_fkey` FOREIGN KEY (`representativeId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `StressfreiEmail` ADD CONSTRAINT `StressfreiEmail_customerId_fkey` FOREIGN KEY (`customerId`) REFERENCES `Customer`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CachedEmail` ADD CONSTRAINT `CachedEmail_stressfreiEmailId_fkey` FOREIGN KEY (`stressfreiEmailId`) REFERENCES `StressfreiEmail`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `CachedEmail` ADD CONSTRAINT `CachedEmail_contractId_fkey` FOREIGN KEY (`contractId`) REFERENCES `Contract`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Contract` ADD CONSTRAINT `Contract_stressfreiEmailId_fkey` FOREIGN KEY (`stressfreiEmailId`) REFERENCES `StressfreiEmail`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `ContractTask` ADD CONSTRAINT `ContractTask_contractId_fkey` FOREIGN KEY (`contractId`) REFERENCES `Contract`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `ContractTaskSubtask` ADD CONSTRAINT `ContractTaskSubtask_taskId_fkey` FOREIGN KEY (`taskId`) REFERENCES `ContractTask`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `User` ADD COLUMN `tokenInvalidatedAt` DATETIME(3) NULL;
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
/**
|
||||||
|
* Datenbank-Restore Script
|
||||||
|
*
|
||||||
|
* Stellt Daten aus einem JSON-Backup wieder her.
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* npx ts-node prisma/restore-data.ts [backup-ordner]
|
||||||
|
*
|
||||||
|
* Beispiele:
|
||||||
|
* npx ts-node prisma/restore-data.ts # Letztes Backup
|
||||||
|
* npx ts-node prisma/restore-data.ts 2025-01-31_14-30-00 # Bestimmtes Backup
|
||||||
|
*
|
||||||
|
* WICHTIG: Führe vorher 'npx prisma migrate deploy' oder 'npx prisma db push' aus!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Hilfsfunktion: JSON-Datei lesen
|
||||||
|
function readJsonFile<T>(filePath: string): T[] {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion: Datum-Strings zu Date-Objekten konvertieren
|
||||||
|
function convertDates(obj: any): any {
|
||||||
|
if (obj === null || obj === undefined) return obj;
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
// ISO-Datumsformat erkennen
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(obj)) {
|
||||||
|
return new Date(obj);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(convertDates);
|
||||||
|
}
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
result[key] = convertDates(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Backup-Ordner bestimmen
|
||||||
|
const backupsDir = path.join(__dirname, 'backups');
|
||||||
|
let backupName = process.argv[2];
|
||||||
|
|
||||||
|
if (!backupName) {
|
||||||
|
// Neuestes Backup finden
|
||||||
|
if (!fs.existsSync(backupsDir)) {
|
||||||
|
console.error('❌ Kein Backup-Ordner gefunden!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const backups = fs.readdirSync(backupsDir)
|
||||||
|
.filter(f => fs.statSync(path.join(backupsDir, f)).isDirectory())
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
if (backups.length === 0) {
|
||||||
|
console.error('❌ Keine Backups gefunden!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
backupName = backups[0];
|
||||||
|
console.log(`📦 Verwende neuestes Backup: ${backupName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupDir = path.join(backupsDir, backupName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupDir)) {
|
||||||
|
console.error(`❌ Backup-Ordner nicht gefunden: ${backupDir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup-Info lesen
|
||||||
|
const infoPath = path.join(backupDir, '_backup-info.json');
|
||||||
|
if (fs.existsSync(infoPath)) {
|
||||||
|
const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8'));
|
||||||
|
console.log(`\n📅 Backup vom: ${new Date(info.timestamp).toLocaleString('de-DE')}`);
|
||||||
|
console.log(`📊 ${info.totalRecords} Datensätze in ${info.tables.filter((t: any) => t.count > 0).length} Tabellen\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 Starte Wiederherstellung aus: ${backupDir}\n`);
|
||||||
|
|
||||||
|
// Foreign Key Checks deaktivieren für MySQL
|
||||||
|
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tabellen in Abhängigkeitsreihenfolge wiederherstellen
|
||||||
|
const restoreOrder = [
|
||||||
|
// Level 0: Keine Abhängigkeiten
|
||||||
|
{
|
||||||
|
name: 'Permission',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.permission.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Role',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.role.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SalesPlatform',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.salesPlatform.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ContractCategory',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractCategory.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CancellationPeriod',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.cancellationPeriod.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ContractDuration',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractDuration.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AppSetting',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.appSetting.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EmailProviderConfig',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.emailProviderConfig.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EnergyProvider',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.energyProvider.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TelecomProvider',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.telecomProvider.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level 1
|
||||||
|
{
|
||||||
|
name: 'RolePermission',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.rolePermission.upsert({
|
||||||
|
where: { roleId_permissionId: { roleId: item.roleId, permissionId: item.permissionId } },
|
||||||
|
update: {},
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'User',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Customer',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.customer.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tariff',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.tariff.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level 2
|
||||||
|
{
|
||||||
|
name: 'UserRole',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.userRole.upsert({
|
||||||
|
where: { userId_roleId: { userId: item.userId, roleId: item.roleId } },
|
||||||
|
update: {},
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CustomerRepresentative',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.customerRepresentative.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'StressfreiEmail',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.stressfreiEmail.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Contract',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contract.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Meter',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.meter.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level 3
|
||||||
|
{
|
||||||
|
name: 'CachedEmail',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.cachedEmail.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ContractTask',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractTask.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MeterReading',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.meterReading.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ContractNote',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractNote.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ContractDocument',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractDocument.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level 4
|
||||||
|
{
|
||||||
|
name: 'ContractTaskSubtask',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.contractTaskSubtask.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Vertragsdetails
|
||||||
|
{
|
||||||
|
name: 'EnergyContractDetails',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.energyContractDetails.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TelecomContractDetails',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.telecomContractDetails.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CarInsuranceDetails',
|
||||||
|
restore: async (data: any[]) => {
|
||||||
|
for (const item of data) {
|
||||||
|
await prisma.carInsuranceDetails.upsert({
|
||||||
|
where: { id: item.id },
|
||||||
|
update: convertDates(item),
|
||||||
|
create: convertDates(item),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let totalRestored = 0;
|
||||||
|
|
||||||
|
for (const table of restoreOrder) {
|
||||||
|
const filePath = path.join(backupDir, `${table.name}.json`);
|
||||||
|
const data = readJsonFile(filePath);
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
console.log(`⚪ ${table.name}: Keine Daten`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await table.restore(data);
|
||||||
|
totalRestored += data.length;
|
||||||
|
console.log(`✅ ${table.name}: ${data.length} Einträge wiederhergestellt`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`⚠️ ${table.name}: Fehler - ${error.message?.slice(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✅ Wiederherstellung abgeschlossen!`);
|
||||||
|
console.log(` 📊 ${totalRestored} Datensätze wiederhergestellt\n`);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Foreign Key Checks wieder aktivieren
|
||||||
|
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('❌ Wiederherstellung fehlgeschlagen:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
+108
-24
@@ -20,17 +20,18 @@ model AppSetting {
|
|||||||
// ==================== USERS & AUTH ====================
|
// ==================== USERS & AUTH ====================
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
firstName String
|
firstName String
|
||||||
lastName String
|
lastName String
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
customerId Int? @unique
|
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||||
customer Customer? @relation(fields: [customerId], references: [id])
|
customerId Int? @unique
|
||||||
roles UserRole[]
|
customer Customer? @relation(fields: [customerId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
roles UserRole[]
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Role {
|
model Role {
|
||||||
@@ -216,6 +217,13 @@ enum EmailProviderType {
|
|||||||
DIRECTADMIN
|
DIRECTADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verschlüsselungstyp für E-Mail-Verbindungen
|
||||||
|
enum MailEncryption {
|
||||||
|
SSL // Implicit SSL/TLS (Ports 465/993) - Verschlüsselung von Anfang an
|
||||||
|
STARTTLS // STARTTLS (Ports 587/143) - Startet unverschlüsselt, dann Upgrade
|
||||||
|
NONE // Keine Verschlüsselung (Ports 25/143)
|
||||||
|
}
|
||||||
|
|
||||||
model EmailProviderConfig {
|
model EmailProviderConfig {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique // z.B. "Plesk Hauptserver"
|
name String @unique // z.B. "Plesk Hauptserver"
|
||||||
@@ -226,6 +234,18 @@ model EmailProviderConfig {
|
|||||||
passwordEncrypted String? // Passwort (verschlüsselt)
|
passwordEncrypted String? // Passwort (verschlüsselt)
|
||||||
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
||||||
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
||||||
|
|
||||||
|
// IMAP/SMTP-Server für E-Mail-Client (optional, default: mail.{domain})
|
||||||
|
imapServer String? // z.B. "mail.stressfrei-wechseln.de"
|
||||||
|
imapPort Int? @default(993)
|
||||||
|
smtpServer String?
|
||||||
|
smtpPort Int? @default(465)
|
||||||
|
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
imapEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||||
|
smtpEncryption MailEncryption @default(SSL) // SSL, STARTTLS oder NONE
|
||||||
|
allowSelfSignedCerts Boolean @default(false) // Selbstsignierte Zertifikate erlauben
|
||||||
|
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isDefault Boolean @default(false) // Standard-Provider
|
isDefault Boolean @default(false) // Standard-Provider
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -235,19 +255,82 @@ model EmailProviderConfig {
|
|||||||
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
||||||
|
|
||||||
model StressfreiEmail {
|
model StressfreiEmail {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customerId Int
|
customerId Int
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||||
notes String? @db.Text // Optionale Notizen
|
notes String? @db.Text // Optionale Notizen
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
|
||||||
createdAt DateTime @default(now())
|
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
|
||||||
updatedAt DateTime @updatedAt
|
hasMailbox Boolean @default(false) // Hat echte Mailbox (nicht nur Weiterleitung)?
|
||||||
|
emailPasswordEncrypted String? // Verschlüsseltes Mailbox-Passwort (AES-256-GCM)
|
||||||
|
|
||||||
|
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||||
|
cachedEmails CachedEmail[] // Gecachte E-Mails aus dieser Mailbox
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CACHED EMAILS (E-Mail-Client) ====================
|
||||||
|
|
||||||
|
enum EmailFolder {
|
||||||
|
INBOX
|
||||||
|
SENT
|
||||||
|
}
|
||||||
|
|
||||||
|
model CachedEmail {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
stressfreiEmailId Int
|
||||||
|
stressfreiEmail StressfreiEmail @relation(fields: [stressfreiEmailId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
// Ordner (Posteingang oder Gesendet)
|
||||||
|
folder EmailFolder @default(INBOX)
|
||||||
|
|
||||||
|
// IMAP-Identifikation
|
||||||
|
messageId String // RFC 5322 Message-ID
|
||||||
|
uid Int // IMAP UID (für Synchronisierung, bei SENT = 0)
|
||||||
|
|
||||||
|
// E-Mail-Metadaten
|
||||||
|
subject String?
|
||||||
|
fromAddress String
|
||||||
|
fromName String?
|
||||||
|
toAddresses String @db.Text // JSON Array
|
||||||
|
ccAddresses String? @db.Text // JSON Array
|
||||||
|
receivedAt DateTime
|
||||||
|
|
||||||
|
// Inhalt
|
||||||
|
textBody String? @db.LongText
|
||||||
|
htmlBody String? @db.LongText
|
||||||
|
hasAttachments Boolean @default(false)
|
||||||
|
attachmentNames String? @db.Text // JSON Array
|
||||||
|
|
||||||
|
// Vertragszuordnung
|
||||||
|
contractId Int?
|
||||||
|
contract Contract? @relation(fields: [contractId], references: [id], onDelete: SetNull)
|
||||||
|
assignedAt DateTime?
|
||||||
|
assignedBy Int? // User ID der die Zuordnung gemacht hat
|
||||||
|
isAutoAssigned Boolean @default(false) // true = automatisch beim Senden aus Vertrag
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
isRead Boolean @default(false)
|
||||||
|
isStarred Boolean @default(false)
|
||||||
|
|
||||||
|
// Papierkorb
|
||||||
|
isDeleted Boolean @default(false) // Im Papierkorb?
|
||||||
|
deletedAt DateTime? // Wann gelöscht?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([stressfreiEmailId, messageId, folder]) // Folder hinzugefügt: gleiche MessageID kann in INBOX und SENT existieren
|
||||||
|
@@index([contractId])
|
||||||
|
@@index([stressfreiEmailId, folder, receivedAt])
|
||||||
|
@@index([stressfreiEmailId, isDeleted]) // Für Papierkorb-Abfragen
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== METERS (Energy) ====================
|
// ==================== METERS (Energy) ====================
|
||||||
@@ -465,6 +548,7 @@ model Contract {
|
|||||||
carInsuranceDetails CarInsuranceDetails?
|
carInsuranceDetails CarInsuranceDetails?
|
||||||
|
|
||||||
tasks ContractTask[]
|
tasks ContractTask[]
|
||||||
|
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
+195
-19
@@ -6,17 +6,31 @@ const prisma = new PrismaClient();
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log('Seeding database...');
|
console.log('Seeding database...');
|
||||||
|
|
||||||
// Create permissions
|
// ==================== PERMISSIONS ====================
|
||||||
const resources = ['customers', 'contracts', 'users', 'platforms', 'providers', 'developer'];
|
// Ressourcen mit ihren erlaubten Aktionen
|
||||||
const actions = ['create', 'read', 'update', 'delete', 'access'];
|
const resourcePermissions: Record<string, string[]> = {
|
||||||
|
// Haupt-Ressourcen (CRUD)
|
||||||
|
customers: ['create', 'read', 'update', 'delete'],
|
||||||
|
contracts: ['create', 'read', 'update', 'delete'],
|
||||||
|
users: ['create', 'read', 'update', 'delete'],
|
||||||
|
platforms: ['create', 'read', 'update', 'delete'],
|
||||||
|
providers: ['create', 'read', 'update', 'delete'],
|
||||||
|
tariffs: ['create', 'read', 'update', 'delete'],
|
||||||
|
// Konfiguration (CRUD)
|
||||||
|
'cancellation-periods': ['create', 'read', 'update', 'delete'],
|
||||||
|
'contract-durations': ['create', 'read', 'update', 'delete'],
|
||||||
|
'contract-categories': ['create', 'read', 'update', 'delete'],
|
||||||
|
'email-providers': ['create', 'read', 'update', 'delete'],
|
||||||
|
// Einstellungen (nur lesen/ändern)
|
||||||
|
settings: ['read', 'update'],
|
||||||
|
// Spezial-Permissions
|
||||||
|
developer: ['access'],
|
||||||
|
emails: ['delete'],
|
||||||
|
};
|
||||||
|
|
||||||
const permissions: { resource: string; action: string }[] = [];
|
const permissions: { resource: string; action: string }[] = [];
|
||||||
for (const resource of resources) {
|
for (const [resource, actions] of Object.entries(resourcePermissions)) {
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
// developer nur mit 'access' action
|
|
||||||
if (resource === 'developer' && action !== 'access') continue;
|
|
||||||
// andere resources ohne 'access' action
|
|
||||||
if (resource !== 'developer' && action === 'access') continue;
|
|
||||||
permissions.push({ resource, action });
|
permissions.push({ resource, action });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +43,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Permissions created');
|
console.log(`Permissions created (${permissions.length} total)`);
|
||||||
|
|
||||||
// Get all permissions
|
// Get all permissions
|
||||||
const allPermissions = await prisma.permission.findMany();
|
const allPermissions = await prisma.permission.findMany();
|
||||||
@@ -63,14 +77,34 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Employee - full access to customers, contracts, read platforms and providers
|
// Developer - ALL permissions including developer:access
|
||||||
|
const developerRole = await prisma.role.upsert({
|
||||||
|
where: { name: 'Developer' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
name: 'Developer',
|
||||||
|
description: 'Voller Zugriff inkl. Entwickler-Tools',
|
||||||
|
permissions: {
|
||||||
|
create: allPermissions.map((p) => ({ permissionId: p.id })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Employee - full access to customers, contracts, read access to lookup tables
|
||||||
const employeePermIds = allPermissions
|
const employeePermIds = allPermissions
|
||||||
.filter(
|
.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.resource === 'customers' ||
|
p.resource === 'customers' ||
|
||||||
p.resource === 'contracts' ||
|
p.resource === 'contracts' ||
|
||||||
(p.resource === 'platforms' && p.action === 'read') ||
|
// Read-only Zugriff auf Stammdaten und Konfiguration
|
||||||
(p.resource === 'providers' && p.action === 'read')
|
(p.action === 'read' && [
|
||||||
|
'platforms',
|
||||||
|
'providers',
|
||||||
|
'tariffs',
|
||||||
|
'cancellation-periods',
|
||||||
|
'contract-durations',
|
||||||
|
'contract-categories',
|
||||||
|
].includes(p.resource))
|
||||||
)
|
)
|
||||||
.map((p) => p.id);
|
.map((p) => p.id);
|
||||||
|
|
||||||
@@ -86,10 +120,20 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read-only employee
|
// Read-only employee - read access to main entities and lookup tables
|
||||||
const readOnlyPermIds = [customerReadPerm?.id, contractReadPerm?.id, platformReadPerm?.id, providerReadPerm?.id].filter(
|
const readOnlyResources = [
|
||||||
(id): id is number => id !== undefined
|
'customers',
|
||||||
);
|
'contracts',
|
||||||
|
'platforms',
|
||||||
|
'providers',
|
||||||
|
'tariffs',
|
||||||
|
'cancellation-periods',
|
||||||
|
'contract-durations',
|
||||||
|
'contract-categories',
|
||||||
|
];
|
||||||
|
const readOnlyPermIds = allPermissions
|
||||||
|
.filter((p) => p.action === 'read' && readOnlyResources.includes(p.resource))
|
||||||
|
.map((p) => p.id);
|
||||||
|
|
||||||
const readOnlyRole = await prisma.role.upsert({
|
const readOnlyRole = await prisma.role.upsert({
|
||||||
where: { name: 'Mitarbeiter (Nur-Lesen)' },
|
where: { name: 'Mitarbeiter (Nur-Lesen)' },
|
||||||
@@ -149,15 +193,76 @@ async function main() {
|
|||||||
|
|
||||||
console.log('Sales platforms created');
|
console.log('Sales platforms created');
|
||||||
|
|
||||||
|
// ==================== STANDARD PROVIDERS ====================
|
||||||
|
const providers = [
|
||||||
|
{
|
||||||
|
name: 'Vodafone',
|
||||||
|
portalUrl: 'https://www.vodafone.de/meinvodafone/account/login',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Klarmobil',
|
||||||
|
portalUrl: 'https://www.klarmobil.de/login',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Otelo',
|
||||||
|
portalUrl: 'https://www.otelo.de/mein-otelo/login',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Congstar',
|
||||||
|
portalUrl: 'https://www.congstar.de/login/',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Telekom',
|
||||||
|
portalUrl: 'https://www.telekom.de/kundencenter/startseite',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'O2',
|
||||||
|
portalUrl: 'https://www.o2online.de/ecare/selfcare',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '1&1',
|
||||||
|
portalUrl: 'https://control-center.1und1.de/',
|
||||||
|
usernameFieldName: 'username',
|
||||||
|
passwordFieldName: 'password',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
await prisma.provider.upsert({
|
||||||
|
where: { name: provider.name },
|
||||||
|
update: {
|
||||||
|
portalUrl: provider.portalUrl,
|
||||||
|
usernameFieldName: provider.usernameFieldName,
|
||||||
|
passwordFieldName: provider.passwordFieldName,
|
||||||
|
},
|
||||||
|
create: { ...provider, isActive: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Providers created');
|
||||||
|
|
||||||
// Create contract categories (matching existing enum values)
|
// Create contract categories (matching existing enum values)
|
||||||
const contractCategories = [
|
const contractCategories = [
|
||||||
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
|
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
|
||||||
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
|
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
|
||||||
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
|
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
|
||||||
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
|
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
|
||||||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 5 },
|
{ code: 'CABLE', name: 'Kabel Internet (Coax)', icon: 'Cable', color: '#00BCD4', sortOrder: 5 },
|
||||||
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 6 },
|
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 6 },
|
||||||
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 7 },
|
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 7 },
|
||||||
|
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 8 },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const category of contractCategories) {
|
for (const category of contractCategories) {
|
||||||
@@ -170,6 +275,77 @@ async function main() {
|
|||||||
|
|
||||||
console.log('Contract categories created');
|
console.log('Contract categories created');
|
||||||
|
|
||||||
|
// ==================== CANCELLATION PERIODS ====================
|
||||||
|
const cancellationPeriods = [
|
||||||
|
{ code: '14D', description: '14 Tage' },
|
||||||
|
{ code: '1M', description: '1 Monat' },
|
||||||
|
{ code: '2M', description: '2 Monate' },
|
||||||
|
{ code: '3M', description: '3 Monate' },
|
||||||
|
{ code: '6M', description: '6 Monate' },
|
||||||
|
{ code: '12M', description: '12 Monate' },
|
||||||
|
{ code: '1W', description: '1 Woche' },
|
||||||
|
{ code: '2W', description: '2 Wochen' },
|
||||||
|
{ code: '4W', description: '4 Wochen' },
|
||||||
|
{ code: '6W', description: '6 Wochen' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const period of cancellationPeriods) {
|
||||||
|
await prisma.cancellationPeriod.upsert({
|
||||||
|
where: { code: period.code },
|
||||||
|
update: { description: period.description },
|
||||||
|
create: period,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cancellation periods created');
|
||||||
|
|
||||||
|
// ==================== CONTRACT DURATIONS ====================
|
||||||
|
const contractDurations = [
|
||||||
|
{ code: '1M', description: '1 Monat' },
|
||||||
|
{ code: '3M', description: '3 Monate' },
|
||||||
|
{ code: '6M', description: '6 Monate' },
|
||||||
|
{ code: '12M', description: '12 Monate' },
|
||||||
|
{ code: '24M', description: '24 Monate' },
|
||||||
|
{ code: '36M', description: '36 Monate' },
|
||||||
|
{ code: '1J', description: '1 Jahr' },
|
||||||
|
{ code: '2J', description: '2 Jahre' },
|
||||||
|
{ code: '3J', description: '3 Jahre' },
|
||||||
|
{ code: '4J', description: '4 Jahre' },
|
||||||
|
{ code: '5J', description: '5 Jahre' },
|
||||||
|
{ code: 'UNBEFRISTET', description: 'Unbefristet' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const duration of contractDurations) {
|
||||||
|
await prisma.contractDuration.upsert({
|
||||||
|
where: { code: duration.code },
|
||||||
|
update: { description: duration.description },
|
||||||
|
create: duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Contract durations created');
|
||||||
|
|
||||||
|
// ==================== APP SETTINGS ====================
|
||||||
|
const appSettings = [
|
||||||
|
// Cockpit-Einstellungen (Fristen-Ampel)
|
||||||
|
{ key: 'deadlineCriticalDays', value: '14' }, // Rot: <= 14 Tage
|
||||||
|
{ key: 'deadlineWarningDays', value: '42' }, // Gelb: <= 42 Tage
|
||||||
|
{ key: 'deadlineOkDays', value: '90' }, // Grün: <= 90 Tage
|
||||||
|
// Allgemeine Einstellungen
|
||||||
|
{ key: 'companyName', value: 'OpenCRM' },
|
||||||
|
{ key: 'defaultEmailDomain', value: 'stressfrei-wechseln.de' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const setting of appSettings) {
|
||||||
|
await prisma.appSetting.upsert({
|
||||||
|
where: { key: setting.key },
|
||||||
|
update: {}, // Bestehende Werte nicht überschreiben
|
||||||
|
create: setting,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('App settings created');
|
||||||
|
|
||||||
console.log('Seeding completed!');
|
console.log('Seeding completed!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as backupService from '../services/backup.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste aller Backups abrufen
|
||||||
|
* GET /api/settings/backups
|
||||||
|
*/
|
||||||
|
export async function listBackups(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const backups = await backupService.listBackups();
|
||||||
|
res.json({ data: backups });
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler beim Laden der Backups', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neues Backup erstellen
|
||||||
|
* POST /api/settings/backup
|
||||||
|
*/
|
||||||
|
export async function createBackup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await backupService.createBackup();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ data: { backupName: result.backupName }, message: 'Backup erfolgreich erstellt' });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Backup fehlgeschlagen', details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler beim Erstellen des Backups', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup wiederherstellen
|
||||||
|
* POST /api/settings/backup/:name/restore
|
||||||
|
*/
|
||||||
|
export async function restoreBackup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Backup-Name erforderlich' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await backupService.restoreBackup(name);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
restoredRecords: result.restoredRecords,
|
||||||
|
restoredFiles: result.restoredFiles,
|
||||||
|
},
|
||||||
|
message: `${result.restoredRecords} Datensätze und ${result.restoredFiles || 0} Dateien wiederhergestellt`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Wiederherstellung fehlgeschlagen', details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler bei der Wiederherstellung', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup löschen
|
||||||
|
* DELETE /api/settings/backup/:name
|
||||||
|
*/
|
||||||
|
export async function deleteBackup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Backup-Name erforderlich' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await backupService.deleteBackup(name);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ message: 'Backup gelöscht' });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Löschen fehlgeschlagen', details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler beim Löschen des Backups', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup als ZIP herunterladen
|
||||||
|
* GET /api/settings/backup/:name/download
|
||||||
|
*/
|
||||||
|
export async function downloadBackup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Backup-Name erforderlich' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await backupService.createBackupZip(name);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
return res.status(404).json({ error: result.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response-Header setzen
|
||||||
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
||||||
|
|
||||||
|
// Archiver zum Response pipen
|
||||||
|
result.stream.pipe(res);
|
||||||
|
|
||||||
|
// Archiver finalisieren (startet das Schreiben)
|
||||||
|
result.stream.finalize();
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler beim Download', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup-ZIP hochladen
|
||||||
|
* POST /api/settings/backup/upload
|
||||||
|
*/
|
||||||
|
export async function uploadBackup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'Keine Datei hochgeladen' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob es eine ZIP-Datei ist
|
||||||
|
if (!req.file.originalname.endsWith('.zip')) {
|
||||||
|
return res.status(400).json({ error: 'Nur ZIP-Dateien sind erlaubt' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await backupService.uploadBackupZip(req.file.buffer);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
data: { backupName: result.backupName },
|
||||||
|
message: 'Backup erfolgreich hochgeladen',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: 'Upload fehlgeschlagen', details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler beim Upload', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Werkseinstellungen - Alle Daten löschen
|
||||||
|
* POST /api/settings/factory-reset
|
||||||
|
*/
|
||||||
|
export async function factoryReset(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await backupService.factoryReset();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({
|
||||||
|
message: 'Werkseinstellungen wiederhergestellt. Bitte melden Sie sich mit admin@admin.com / admin an.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ error: 'Werkseinstellungen fehlgeschlagen', details: result.error });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: 'Fehler bei Werkseinstellungen', details: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,805 @@
|
|||||||
|
// ==================== CACHED EMAIL CONTROLLER ====================
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as cachedEmailService from '../services/cachedEmail.service.js';
|
||||||
|
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||||||
|
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js';
|
||||||
|
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
|
||||||
|
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
||||||
|
import { decrypt } from '../utils/encryption.js';
|
||||||
|
import { ApiResponse } from '../types/index.js';
|
||||||
|
|
||||||
|
// ==================== E-MAIL LIST ====================
|
||||||
|
|
||||||
|
// E-Mails für einen Kunden abrufen
|
||||||
|
export async function getEmailsForCustomer(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const customerId = parseInt(req.params.customerId);
|
||||||
|
const stressfreiEmailId = req.query.accountId ? parseInt(req.query.accountId as string) : undefined;
|
||||||
|
const folder = req.query.folder as string | undefined; // INBOX oder SENT
|
||||||
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
|
||||||
|
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0;
|
||||||
|
|
||||||
|
const emails = await cachedEmailService.getCachedEmails({
|
||||||
|
customerId,
|
||||||
|
stressfreiEmailId,
|
||||||
|
folder,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
includeBody: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: emails } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getEmailsForCustomer error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mails für einen Vertrag abrufen
|
||||||
|
export async function getEmailsForContract(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const contractId = parseInt(req.params.contractId);
|
||||||
|
const folder = req.query.folder as string | undefined; // INBOX oder SENT
|
||||||
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
|
||||||
|
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0;
|
||||||
|
|
||||||
|
const emails = await cachedEmailService.getCachedEmails({
|
||||||
|
contractId,
|
||||||
|
folder,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
includeBody: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: emails } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getEmailsForContract error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Vertrags-E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SINGLE EMAIL ====================
|
||||||
|
|
||||||
|
// Einzelne E-Mail abrufen (mit Body)
|
||||||
|
export async function getEmail(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const email = await cachedEmailService.getCachedEmailById(id);
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als gelesen markieren
|
||||||
|
await cachedEmailService.markEmailAsRead(id);
|
||||||
|
|
||||||
|
res.json({ success: true, data: { ...email, isRead: true } } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getEmail error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail als gelesen/ungelesen markieren
|
||||||
|
export async function markAsRead(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const { isRead } = req.body;
|
||||||
|
|
||||||
|
if (isRead) {
|
||||||
|
await cachedEmailService.markEmailAsRead(id);
|
||||||
|
} else {
|
||||||
|
await cachedEmailService.markEmailAsUnread(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('markAsRead error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Markieren der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail Stern umschalten
|
||||||
|
export async function toggleStar(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const isStarred = await cachedEmailService.toggleEmailStar(id);
|
||||||
|
|
||||||
|
res.json({ success: true, data: { isStarred } } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('toggleStar error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Ändern des Sterns',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CONTRACT ASSIGNMENT ====================
|
||||||
|
|
||||||
|
// E-Mail einem Vertrag zuordnen
|
||||||
|
export async function assignToContract(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const emailId = parseInt(req.params.id);
|
||||||
|
const { contractId } = req.body;
|
||||||
|
const userId = (req as any).userId; // Falls Auth-Middleware userId setzt
|
||||||
|
|
||||||
|
const email = await cachedEmailService.assignEmailToContract(emailId, contractId, userId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: email } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('assignToContract error:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Zuordnen der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertragszuordnung aufheben
|
||||||
|
export async function unassignFromContract(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const emailId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const email = await cachedEmailService.unassignEmailFromContract(emailId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: email } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('unassignFromContract error:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Aufheben der Zuordnung',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Anzahl pro Ordner für ein Konto
|
||||||
|
export async function getFolderCounts(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const stressfreiEmailId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const counts = await cachedEmailService.getFolderCountsForAccount(stressfreiEmailId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: counts } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getFolderCounts error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Ordner-Anzahlen',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Anzahl pro Ordner für einen Vertrag
|
||||||
|
export async function getContractFolderCounts(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const contractId = parseInt(req.params.contractId);
|
||||||
|
|
||||||
|
const counts = await cachedEmailService.getFolderCountsForContract(contractId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: counts } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getContractFolderCounts error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Ordner-Anzahlen',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SYNC & SEND ====================
|
||||||
|
|
||||||
|
// E-Mails für ein Konto synchronisieren (INBOX + SENT)
|
||||||
|
export async function syncAccount(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const stressfreiEmailId = parseInt(req.params.id);
|
||||||
|
const fullSync = req.query.full === 'true';
|
||||||
|
|
||||||
|
// Synchronisiert sowohl INBOX als auch SENT
|
||||||
|
const result = await cachedEmailService.syncAllFoldersForAccount(stressfreiEmailId, { fullSync });
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
newEmails: result.newEmails,
|
||||||
|
totalEmails: result.totalEmails,
|
||||||
|
},
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('syncAccount error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Synchronisieren der E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
export async function sendEmailFromAccount(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const stressfreiEmailId = parseInt(req.params.id);
|
||||||
|
const { to, cc, subject, text, html, inReplyTo, references, attachments, contractId } = req.body;
|
||||||
|
|
||||||
|
// StressfreiEmail laden
|
||||||
|
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(stressfreiEmailId);
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail-Konto nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stressfreiEmail.hasMailbox) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Dieses Konto hat keine Mailbox für den Versand',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort entschlüsseln
|
||||||
|
const password = await stressfreiEmailService.getDecryptedPassword(stressfreiEmailId);
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Passwort für E-Mail-Versand nicht verfügbar',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP-Einstellungen vom Provider
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine SMTP-Einstellungen konfiguriert',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP-Credentials
|
||||||
|
const credentials: SmtpCredentials = {
|
||||||
|
host: settings.smtpServer,
|
||||||
|
port: settings.smtpPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.smtpEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// E-Mail-Parameter
|
||||||
|
const emailParams: SendEmailParams = {
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
inReplyTo,
|
||||||
|
references,
|
||||||
|
attachments: attachments as EmailAttachment[] | undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
const result = await sendEmail(credentials, stressfreiEmail.email, emailParams);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesendete E-Mail im IMAP Sent-Ordner speichern (für Attachment-Download)
|
||||||
|
let sentUid: number | undefined;
|
||||||
|
if (result.rawEmail) {
|
||||||
|
try {
|
||||||
|
// IMAP-Credentials für Sent-Ordner
|
||||||
|
const imapCredentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendResult = await appendToSent(imapCredentials, {
|
||||||
|
rawEmail: result.rawEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (appendResult.success && appendResult.uid) {
|
||||||
|
sentUid = appendResult.uid;
|
||||||
|
console.log(`[SMTP] Email stored in Sent folder with UID ${sentUid}`);
|
||||||
|
}
|
||||||
|
} catch (appendError) {
|
||||||
|
// Nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||||
|
console.error('Error appending to IMAP Sent folder:', appendError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesendete E-Mail im Cache speichern
|
||||||
|
try {
|
||||||
|
// Anhangsnamen extrahieren falls vorhanden
|
||||||
|
const attachmentNames = attachments?.map((a: EmailAttachment) => a.filename) || [];
|
||||||
|
|
||||||
|
await cachedEmailService.createSentEmail(stressfreiEmailId, {
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
messageId: result.messageId || `sent-${Date.now()}@opencrm.local`,
|
||||||
|
contractId: contractId ? parseInt(contractId) : undefined,
|
||||||
|
attachmentNames: attachmentNames.length > 0 ? attachmentNames : undefined,
|
||||||
|
uid: sentUid, // UID vom IMAP Sent-Ordner für Attachment-Download
|
||||||
|
});
|
||||||
|
} catch (saveError) {
|
||||||
|
// Fehler beim Speichern nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||||
|
console.error('Error saving sent email to cache:', saveError);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { messageId: result.messageId },
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('sendEmailFromAccount error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Senden der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== ATTACHMENTS ====================
|
||||||
|
|
||||||
|
// Anhang-Liste einer E-Mail abrufen
|
||||||
|
export async function getAttachments(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const emailId = parseInt(req.params.emailId);
|
||||||
|
|
||||||
|
// E-Mail aus Cache laden
|
||||||
|
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||||
|
if (!email) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anhänge aus attachmentNames parsen (JSON Array)
|
||||||
|
const attachmentNames: string[] = email.attachmentNames
|
||||||
|
? JSON.parse(email.attachmentNames)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: attachmentNames.map((name) => ({ filename: name })),
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getAttachments error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Anhänge',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einzelnen Anhang herunterladen
|
||||||
|
export async function downloadAttachment(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const emailId = parseInt(req.params.emailId);
|
||||||
|
const filename = decodeURIComponent(req.params.filename);
|
||||||
|
|
||||||
|
// E-Mail aus Cache laden
|
||||||
|
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||||
|
if (!email) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für gesendete E-Mails: Prüfen ob UID vorhanden (im IMAP Sent gespeichert)
|
||||||
|
if (email.folder === 'SENT' && email.uid === 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// StressfreiEmail laden um Zugangsdaten zu bekommen
|
||||||
|
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
|
||||||
|
if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine Mailbox-Zugangsdaten verfügbar',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP-Einstellungen laden
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine E-Mail-Provider-Einstellungen gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort entschlüsseln
|
||||||
|
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
|
||||||
|
// IMAP-Credentials
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ordner basierend auf E-Mail-Typ bestimmen (INBOX oder Sent)
|
||||||
|
const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||||
|
|
||||||
|
// Anhang per IMAP abrufen
|
||||||
|
const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
|
||||||
|
|
||||||
|
if (!attachment) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Anhang nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datei senden - inline (öffnen) oder attachment (download)
|
||||||
|
const disposition = req.query.view === 'true' ? 'inline' : 'attachment';
|
||||||
|
res.setHeader('Content-Type', attachment.contentType);
|
||||||
|
res.setHeader('Content-Disposition', `${disposition}; filename="${encodeURIComponent(attachment.filename)}"`);
|
||||||
|
res.setHeader('Content-Length', attachment.size);
|
||||||
|
res.send(attachment.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('downloadAttachment error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Herunterladen des Anhangs',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MAILBOX ACCOUNTS ====================
|
||||||
|
|
||||||
|
// Mailbox-Konten eines Kunden abrufen
|
||||||
|
export async function getMailboxAccounts(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const customerId = parseInt(req.params.customerId);
|
||||||
|
|
||||||
|
const accounts = await cachedEmailService.getMailboxAccountsForCustomer(customerId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: accounts } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getMailboxAccounts error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der E-Mail-Konten',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox nachträglich aktivieren
|
||||||
|
export async function enableMailbox(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const result = await stressfreiEmailService.enableMailbox(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('enableMailbox error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Aktivieren der Mailbox',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox-Status mit Provider synchronisieren
|
||||||
|
export async function syncMailboxStatus(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const result = await stressfreiEmailService.syncMailboxStatus(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
hasMailbox: result.hasMailbox,
|
||||||
|
wasUpdated: result.wasUpdated,
|
||||||
|
},
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('syncMailboxStatus error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Synchronisieren des Mailbox-Status',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Thread abrufen
|
||||||
|
export async function getThread(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const thread = await cachedEmailService.getEmailThread(id);
|
||||||
|
|
||||||
|
res.json({ success: true, data: thread } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getThread error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden des E-Mail-Threads',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox-Zugangsdaten abrufen (IMAP/SMTP)
|
||||||
|
export async function getMailboxCredentials(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
// StressfreiEmail laden
|
||||||
|
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(id);
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail-Konto nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stressfreiEmail.hasMailbox) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine Mailbox für diese E-Mail-Adresse',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort entschlüsseln
|
||||||
|
const password = await stressfreiEmailService.getDecryptedPassword(id);
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Passwort konnte nicht entschlüsselt werden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP/SMTP-Einstellungen laden
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
email: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
imap: settings ? {
|
||||||
|
server: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
} : null,
|
||||||
|
smtp: settings ? {
|
||||||
|
server: settings.smtpServer,
|
||||||
|
port: settings.smtpPort,
|
||||||
|
encryption: settings.smtpEncryption,
|
||||||
|
} : null,
|
||||||
|
},
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getMailboxCredentials error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Mailbox-Zugangsdaten',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ungelesene E-Mails zählen
|
||||||
|
export async function getUnreadCount(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const customerId = req.query.customerId ? parseInt(req.query.customerId as string) : undefined;
|
||||||
|
const contractId = req.query.contractId ? parseInt(req.query.contractId as string) : undefined;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
if (customerId) {
|
||||||
|
count = await cachedEmailService.getUnreadCountForCustomer(customerId);
|
||||||
|
} else if (contractId) {
|
||||||
|
count = await cachedEmailService.getUnreadCountForContract(contractId);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, data: { count } } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getUnreadCount error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Zählen der ungelesenen E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail in Papierkorb verschieben (nur Admin)
|
||||||
|
export async function deleteEmail(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const email = await cachedEmailService.getCachedEmailById(id);
|
||||||
|
if (!email) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail nicht gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await cachedEmailService.moveEmailToTrash(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'E-Mail in Papierkorb verschoben' } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('deleteEmail error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Löschen der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TRASH OPERATIONS ====================
|
||||||
|
|
||||||
|
// Papierkorb-E-Mails für einen Kunden abrufen
|
||||||
|
export async function getTrashEmails(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const customerId = parseInt(req.params.customerId);
|
||||||
|
|
||||||
|
const emails = await cachedEmailService.getTrashEmails(customerId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: emails } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getTrashEmails error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Laden der Papierkorb-E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Papierkorb-Anzahl für einen Kunden
|
||||||
|
export async function getTrashCount(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const customerId = parseInt(req.params.customerId);
|
||||||
|
|
||||||
|
const count = await cachedEmailService.getTrashCount(customerId);
|
||||||
|
|
||||||
|
res.json({ success: true, data: { count } } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getTrashCount error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Zählen der Papierkorb-E-Mails',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail aus Papierkorb wiederherstellen
|
||||||
|
export async function restoreEmail(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const result = await cachedEmailService.restoreEmailFromTrash(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'E-Mail wiederhergestellt' } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('restoreEmail error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim Wiederherstellen der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail endgültig löschen (aus Papierkorb)
|
||||||
|
export async function permanentDeleteEmail(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const result = await cachedEmailService.permanentDeleteEmail(id);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'E-Mail endgültig gelöscht' } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('permanentDeleteEmail error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Fehler beim endgültigen Löschen der E-Mail',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,3 +74,26 @@ export async function deleteEmail(req: Request, res: Response): Promise<void> {
|
|||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await stressfreiEmailService.resetMailboxPassword(parseInt(req.params.id));
|
||||||
|
if (!result.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { password: result.password },
|
||||||
|
message: 'Passwort wurde zurückgesetzt',
|
||||||
|
} as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen des Passworts',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import contractCategoryRoutes from './routes/contractCategory.routes.js';
|
|||||||
import contractTaskRoutes from './routes/contractTask.routes.js';
|
import contractTaskRoutes from './routes/contractTask.routes.js';
|
||||||
import appSettingRoutes from './routes/appSetting.routes.js';
|
import appSettingRoutes from './routes/appSetting.routes.js';
|
||||||
import emailProviderRoutes from './routes/emailProvider.routes.js';
|
import emailProviderRoutes from './routes/emailProvider.routes.js';
|
||||||
|
import cachedEmailRoutes from './routes/cachedEmail.routes.js';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ app.use('/api/contract-categories', contractCategoryRoutes);
|
|||||||
app.use('/api', contractTaskRoutes);
|
app.use('/api', contractTaskRoutes);
|
||||||
app.use('/api/settings', appSettingRoutes);
|
app.use('/api/settings', appSettingRoutes);
|
||||||
app.use('/api/email-providers', emailProviderRoutes);
|
app.use('/api/email-providers', emailProviderRoutes);
|
||||||
|
app.use('/api', cachedEmailRoutes);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
|
|||||||
@@ -1,26 +1,64 @@
|
|||||||
import { Response, NextFunction } from 'express';
|
import { Response, NextFunction } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { AuthRequest, JwtPayload } from '../types/index.js';
|
import { AuthRequest, JwtPayload } from '../types/index.js';
|
||||||
|
|
||||||
export function authenticate(
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function authenticate(
|
||||||
req: AuthRequest,
|
req: AuthRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): void {
|
): Promise<void> {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
// Token aus Header oder Query-Parameter (für Downloads)
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
token = authHeader.split(' ')[1];
|
||||||
|
} else if (req.query.token && typeof req.query.token === 'string') {
|
||||||
|
// Fallback für Downloads: Token als Query-Parameter
|
||||||
|
token = req.query.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(
|
const decoded = jwt.verify(
|
||||||
token,
|
token,
|
||||||
process.env.JWT_SECRET || 'fallback-secret'
|
process.env.JWT_SECRET || 'fallback-secret'
|
||||||
) as JwtPayload;
|
) as JwtPayload;
|
||||||
|
|
||||||
|
// Prüfen ob Token durch Rechteänderung invalidiert wurde (nur für Mitarbeiter)
|
||||||
|
if (decoded.userId && decoded.iat) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.userId },
|
||||||
|
select: { tokenInvalidatedAt: true, isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Benutzer nicht gefunden oder deaktiviert
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
res.status(401).json({ success: false, error: 'Benutzer nicht mehr aktiv' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token wurde vor der Invalidierung ausgestellt
|
||||||
|
if (user.tokenInvalidatedAt) {
|
||||||
|
const tokenIssuedAt = decoded.iat * 1000; // iat ist in Sekunden, Date ist in Millisekunden
|
||||||
|
if (tokenIssuedAt < user.tokenInvalidatedAt.getTime()) {
|
||||||
|
res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ihre Berechtigungen wurden geändert. Bitte melden Sie sich erneut an.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.user = decoded;
|
req.user = decoded;
|
||||||
next();
|
next();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
import * as appSettingController from '../controllers/appSetting.controller.js';
|
import * as appSettingController from '../controllers/appSetting.controller.js';
|
||||||
|
import * as backupController from '../controllers/backup.controller.js';
|
||||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||||
|
|
||||||
|
// Multer für Backup-Upload (in Memory speichern)
|
||||||
|
const backupUpload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: { fileSize: 500 * 1024 * 1024 }, // 500MB max
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype === 'application/zip' || file.originalname.endsWith('.zip')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Nur ZIP-Dateien sind erlaubt'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
||||||
@@ -26,4 +41,63 @@ router.put(
|
|||||||
appSettingController.updateSettings
|
appSettingController.updateSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ==================== BACKUP & RESTORE ====================
|
||||||
|
|
||||||
|
// Liste aller Backups
|
||||||
|
router.get(
|
||||||
|
'/backups',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.listBackups
|
||||||
|
);
|
||||||
|
|
||||||
|
// Neues Backup erstellen
|
||||||
|
router.post(
|
||||||
|
'/backup',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.createBackup
|
||||||
|
);
|
||||||
|
|
||||||
|
// Backup wiederherstellen
|
||||||
|
router.post(
|
||||||
|
'/backup/:name/restore',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.restoreBackup
|
||||||
|
);
|
||||||
|
|
||||||
|
// Backup löschen
|
||||||
|
router.delete(
|
||||||
|
'/backup/:name',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.deleteBackup
|
||||||
|
);
|
||||||
|
|
||||||
|
// Backup als ZIP herunterladen
|
||||||
|
router.get(
|
||||||
|
'/backup/:name/download',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.downloadBackup
|
||||||
|
);
|
||||||
|
|
||||||
|
// Backup-ZIP hochladen
|
||||||
|
router.post(
|
||||||
|
'/backup/upload',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupUpload.single('backup'),
|
||||||
|
backupController.uploadBackup
|
||||||
|
);
|
||||||
|
|
||||||
|
// Werkseinstellungen (alles löschen)
|
||||||
|
router.post(
|
||||||
|
'/factory-reset',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('settings:update'),
|
||||||
|
backupController.factoryReset
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// ==================== CACHED EMAIL ROUTES ====================
|
||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import * as cachedEmailController from '../controllers/cachedEmail.controller.js';
|
||||||
|
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// ==================== E-MAIL LISTEN ====================
|
||||||
|
|
||||||
|
// E-Mails für Kunden (mit optionalem Account-Filter)
|
||||||
|
// GET /api/customers/:customerId/emails?accountId=1&limit=50&offset=0
|
||||||
|
router.get(
|
||||||
|
'/customers/:customerId/emails',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getEmailsForCustomer
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mails für Vertrag
|
||||||
|
// GET /api/contracts/:contractId/emails?limit=50&offset=0
|
||||||
|
router.get(
|
||||||
|
'/contracts/:contractId/emails',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:read'),
|
||||||
|
cachedEmailController.getEmailsForContract
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ordner-Anzahlen für Vertrag (zugeordnete E-Mails)
|
||||||
|
// GET /api/contracts/:contractId/emails/folder-counts
|
||||||
|
router.get(
|
||||||
|
'/contracts/:contractId/emails/folder-counts',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:read'),
|
||||||
|
cachedEmailController.getContractFolderCounts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mailbox-Konten eines Kunden (für Dropdown)
|
||||||
|
// GET /api/customers/:customerId/mailbox-accounts
|
||||||
|
router.get(
|
||||||
|
'/customers/:customerId/mailbox-accounts',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getMailboxAccounts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ungelesene E-Mails zählen
|
||||||
|
// GET /api/emails/unread-count?customerId=1 oder ?contractId=1
|
||||||
|
router.get(
|
||||||
|
'/emails/unread-count',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getUnreadCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== EINZELNE E-MAIL ====================
|
||||||
|
|
||||||
|
// Einzelne E-Mail abrufen (mit Body, markiert als gelesen)
|
||||||
|
// GET /api/emails/:id
|
||||||
|
router.get(
|
||||||
|
'/emails/:id',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getEmail
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mail in Papierkorb verschieben (nur User mit emails:delete Permission)
|
||||||
|
// DELETE /api/emails/:id
|
||||||
|
router.delete(
|
||||||
|
'/emails/:id',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('emails:delete'),
|
||||||
|
cachedEmailController.deleteEmail
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== PAPIERKORB ====================
|
||||||
|
|
||||||
|
// Papierkorb-E-Mails für Kunden abrufen
|
||||||
|
// GET /api/customers/:customerId/emails/trash
|
||||||
|
router.get(
|
||||||
|
'/customers/:customerId/emails/trash',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('emails:delete'),
|
||||||
|
cachedEmailController.getTrashEmails
|
||||||
|
);
|
||||||
|
|
||||||
|
// Papierkorb-Anzahl für Kunden
|
||||||
|
// GET /api/customers/:customerId/emails/trash/count
|
||||||
|
router.get(
|
||||||
|
'/customers/:customerId/emails/trash/count',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('emails:delete'),
|
||||||
|
cachedEmailController.getTrashCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mail aus Papierkorb wiederherstellen
|
||||||
|
// POST /api/emails/:id/restore
|
||||||
|
router.post(
|
||||||
|
'/emails/:id/restore',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('emails:delete'),
|
||||||
|
cachedEmailController.restoreEmail
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mail endgültig löschen (nur aus Papierkorb)
|
||||||
|
// DELETE /api/emails/:id/permanent
|
||||||
|
router.delete(
|
||||||
|
'/emails/:id/permanent',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('emails:delete'),
|
||||||
|
cachedEmailController.permanentDeleteEmail
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mail-Thread abrufen
|
||||||
|
// GET /api/emails/:id/thread
|
||||||
|
router.get(
|
||||||
|
'/emails/:id/thread',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getThread
|
||||||
|
);
|
||||||
|
|
||||||
|
// Als gelesen/ungelesen markieren
|
||||||
|
// PATCH /api/emails/:id/read
|
||||||
|
router.patch(
|
||||||
|
'/emails/:id/read',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:update'),
|
||||||
|
cachedEmailController.markAsRead
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stern umschalten
|
||||||
|
// POST /api/emails/:id/star
|
||||||
|
router.post(
|
||||||
|
'/emails/:id/star',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:update'),
|
||||||
|
cachedEmailController.toggleStar
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== ANHÄNGE ====================
|
||||||
|
|
||||||
|
// Anhang-Liste einer E-Mail
|
||||||
|
// GET /api/emails/:emailId/attachments
|
||||||
|
router.get(
|
||||||
|
'/emails/:emailId/attachments',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getAttachments
|
||||||
|
);
|
||||||
|
|
||||||
|
// Einzelnen Anhang herunterladen
|
||||||
|
// GET /api/emails/:emailId/attachments/:filename
|
||||||
|
router.get(
|
||||||
|
'/emails/:emailId/attachments/:filename',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.downloadAttachment
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== VERTRAGSZUORDNUNG ====================
|
||||||
|
|
||||||
|
// E-Mail Vertrag zuordnen
|
||||||
|
// POST /api/emails/:id/assign { contractId: number }
|
||||||
|
router.post(
|
||||||
|
'/emails/:id/assign',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:update'),
|
||||||
|
cachedEmailController.assignToContract
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zuordnung aufheben
|
||||||
|
// DELETE /api/emails/:id/assign
|
||||||
|
router.delete(
|
||||||
|
'/emails/:id/assign',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:update'),
|
||||||
|
cachedEmailController.unassignFromContract
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== STRESSFREI-EMAIL OPERATIONEN ====================
|
||||||
|
|
||||||
|
// E-Mails für ein Konto synchronisieren
|
||||||
|
// POST /api/stressfrei-emails/:id/sync?full=true
|
||||||
|
router.post(
|
||||||
|
'/stressfrei-emails/:id/sync',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:update'),
|
||||||
|
cachedEmailController.syncAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
// POST /api/stressfrei-emails/:id/send { to, cc, subject, text, html, inReplyTo, references }
|
||||||
|
router.post(
|
||||||
|
'/stressfrei-emails/:id/send',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:update'),
|
||||||
|
cachedEmailController.sendEmailFromAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mailbox nachträglich aktivieren
|
||||||
|
// POST /api/stressfrei-emails/:id/enable-mailbox
|
||||||
|
router.post(
|
||||||
|
'/stressfrei-emails/:id/enable-mailbox',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:update'),
|
||||||
|
cachedEmailController.enableMailbox
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mailbox-Status mit Provider synchronisieren
|
||||||
|
// POST /api/stressfrei-emails/:id/sync-mailbox-status
|
||||||
|
router.post(
|
||||||
|
'/stressfrei-emails/:id/sync-mailbox-status',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.syncMailboxStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mailbox-Zugangsdaten abrufen (IMAP/SMTP)
|
||||||
|
// GET /api/stressfrei-emails/:id/credentials
|
||||||
|
router.get(
|
||||||
|
'/stressfrei-emails/:id/credentials',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getMailboxCredentials
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ordner-Anzahlen für ein Konto (INBOX, SENT, ungelesen)
|
||||||
|
// GET /api/stressfrei-emails/:id/folder-counts
|
||||||
|
router.get(
|
||||||
|
'/stressfrei-emails/:id/folder-counts',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('customers:read'),
|
||||||
|
cachedEmailController.getFolderCounts
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -4,10 +4,13 @@ import { authenticate, requirePermission } from '../middleware/auth.js';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Lesen für alle authentifizierten Benutzer
|
||||||
router.get('/', authenticate, contractCategoryController.getContractCategories);
|
router.get('/', authenticate, contractCategoryController.getContractCategories);
|
||||||
router.post('/', authenticate, requirePermission('platforms:create'), contractCategoryController.createContractCategory);
|
|
||||||
router.get('/:id', authenticate, contractCategoryController.getContractCategory);
|
router.get('/:id', authenticate, contractCategoryController.getContractCategory);
|
||||||
router.put('/:id', authenticate, requirePermission('platforms:update'), contractCategoryController.updateContractCategory);
|
|
||||||
router.delete('/:id', authenticate, requirePermission('platforms:delete'), contractCategoryController.deleteContractCategory);
|
// Ändern/Löschen nur mit Entwickler-Berechtigung (Vertragstypen erfordern Formular-Anpassungen)
|
||||||
|
router.post('/', authenticate, requirePermission('developer:access'), contractCategoryController.createContractCategory);
|
||||||
|
router.put('/:id', authenticate, requirePermission('developer:access'), contractCategoryController.updateContractCategory);
|
||||||
|
router.delete('/:id', authenticate, requirePermission('developer:access'), contractCategoryController.deleteContractCategory);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ router.get('/:id', authenticate, requirePermission('customers:read'), stressfrei
|
|||||||
router.put('/:id', authenticate, requirePermission('customers:update'), stressfreiEmailController.updateEmail);
|
router.put('/:id', authenticate, requirePermission('customers:update'), stressfreiEmailController.updateEmail);
|
||||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), stressfreiEmailController.deleteEmail);
|
router.delete('/:id', authenticate, requirePermission('customers:delete'), stressfreiEmailController.deleteEmail);
|
||||||
|
|
||||||
|
// Passwort zurücksetzen (generiert neues Passwort und setzt es beim Provider)
|
||||||
|
router.post('/:id/reset-password', authenticate, requirePermission('customers:update'), stressfreiEmailController.resetPassword);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,957 @@
|
|||||||
|
// ==================== CACHED EMAIL SERVICE ====================
|
||||||
|
// Service für E-Mail-Caching und Vertragszuordnung
|
||||||
|
|
||||||
|
import { PrismaClient, CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
||||||
|
import { decrypt } from '../utils/encryption.js';
|
||||||
|
import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete } from './imapService.js';
|
||||||
|
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// ==================== TYPES ====================
|
||||||
|
|
||||||
|
export interface CachedEmailWithRelations extends CachedEmail {
|
||||||
|
stressfreiEmail?: {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
customerId: number;
|
||||||
|
};
|
||||||
|
contract?: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter für gesendete E-Mail
|
||||||
|
export interface SentEmailParams {
|
||||||
|
to: string[];
|
||||||
|
cc?: string[];
|
||||||
|
subject: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
messageId: string;
|
||||||
|
contractId?: number; // Optional: Vertrag dem die E-Mail zugeordnet wird
|
||||||
|
attachmentNames?: string[]; // Namen der Anhänge
|
||||||
|
uid?: number; // UID im IMAP Sent-Ordner (für Attachment-Download)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResult {
|
||||||
|
success: boolean;
|
||||||
|
newEmails: number;
|
||||||
|
totalEmails: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailListOptions {
|
||||||
|
stressfreiEmailId?: number;
|
||||||
|
customerId?: number;
|
||||||
|
contractId?: number;
|
||||||
|
folder?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
includeBody?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SYNC FUNCTIONS ====================
|
||||||
|
|
||||||
|
// E-Mails für eine StressfreiEmail synchronisieren
|
||||||
|
export async function syncEmailsForAccount(
|
||||||
|
stressfreiEmailId: number,
|
||||||
|
options?: { folder?: string; fullSync?: boolean }
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
const folder = options?.folder || 'INBOX';
|
||||||
|
const fullSync = options?.fullSync || false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// StressfreiEmail mit Mailbox-Daten laden
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id: stressfreiEmailId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, newEmails: 0, totalEmails: 0, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stressfreiEmail.hasMailbox || !stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
return { success: false, newEmails: 0, totalEmails: 0, error: 'Keine Mailbox für diese E-Mail-Adresse' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP/SMTP-Einstellungen vom Provider holen
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
return { success: false, newEmails: 0, totalEmails: 0, error: 'Keine E-Mail-Provider-Einstellungen gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort entschlüsseln
|
||||||
|
let password: string;
|
||||||
|
try {
|
||||||
|
password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
return { success: false, newEmails: 0, totalEmails: 0, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP-Credentials zusammenstellen
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Folder-Mapping: IMAP-Ordner zu DB-Ordner
|
||||||
|
const dbFolder = folder.toUpperCase() === 'SENT' || folder.toLowerCase() === 'sent'
|
||||||
|
? EmailFolder.SENT
|
||||||
|
: EmailFolder.INBOX;
|
||||||
|
|
||||||
|
// Für inkrementellen Sync: Höchste UID des Ordners ermitteln
|
||||||
|
let sinceUid: number | undefined;
|
||||||
|
if (!fullSync) {
|
||||||
|
const lastEmail = await prisma.cachedEmail.findFirst({
|
||||||
|
where: { stressfreiEmailId, folder: dbFolder },
|
||||||
|
orderBy: { uid: 'desc' },
|
||||||
|
select: { uid: true },
|
||||||
|
});
|
||||||
|
if (lastEmail) {
|
||||||
|
sinceUid = lastEmail.uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mails vom IMAP-Server abrufen
|
||||||
|
const fetchedEmails = await fetchEmails(credentials, {
|
||||||
|
folder,
|
||||||
|
sinceUid,
|
||||||
|
limit: fullSync ? 500 : 100, // Mehr bei Full-Sync
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neue E-Mails in DB speichern
|
||||||
|
let newCount = 0;
|
||||||
|
for (const email of fetchedEmails) {
|
||||||
|
const created = await upsertCachedEmail(stressfreiEmailId, email, dbFolder);
|
||||||
|
if (created) newCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesamtzahl ermitteln
|
||||||
|
const totalEmails = await prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
newEmails: newCount,
|
||||||
|
totalEmails,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('syncEmailsForAccount error:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
newEmails: 0,
|
||||||
|
totalEmails: 0,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Ordner synchronisieren (INBOX + SENT)
|
||||||
|
export async function syncAllFoldersForAccount(
|
||||||
|
stressfreiEmailId: number,
|
||||||
|
options?: { fullSync?: boolean }
|
||||||
|
): Promise<SyncResult> {
|
||||||
|
const fullSync = options?.fullSync || false;
|
||||||
|
|
||||||
|
// INBOX synchronisieren
|
||||||
|
const inboxResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'INBOX', fullSync });
|
||||||
|
|
||||||
|
// SENT synchronisieren (Plesk verwendet "Sent" als Ordnername)
|
||||||
|
const sentResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'Sent', fullSync });
|
||||||
|
|
||||||
|
// Ergebnisse kombinieren
|
||||||
|
if (!inboxResult.success && !sentResult.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
newEmails: 0,
|
||||||
|
totalEmails: 0,
|
||||||
|
error: inboxResult.error || sentResult.error || 'Synchronisation fehlgeschlagen',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
newEmails: inboxResult.newEmails + sentResult.newEmails,
|
||||||
|
totalEmails: inboxResult.totalEmails,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einzelne E-Mail in DB speichern/aktualisieren
|
||||||
|
async function upsertCachedEmail(
|
||||||
|
stressfreiEmailId: number,
|
||||||
|
email: FetchedEmail,
|
||||||
|
folder: EmailFolder = EmailFolder.INBOX
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Prüfen ob bereits vorhanden (via messageId + folder)
|
||||||
|
// WICHTIG: Gleiche messageId kann in INBOX und SENT existieren (z.B. E-Mail an sich selbst)
|
||||||
|
const existing = await prisma.cachedEmail.findUnique({
|
||||||
|
where: {
|
||||||
|
stressfreiEmailId_messageId_folder: {
|
||||||
|
stressfreiEmailId,
|
||||||
|
messageId: email.messageId,
|
||||||
|
folder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Bereits vorhanden
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue E-Mail anlegen
|
||||||
|
await prisma.cachedEmail.create({
|
||||||
|
data: {
|
||||||
|
stressfreiEmailId,
|
||||||
|
folder,
|
||||||
|
messageId: email.messageId,
|
||||||
|
uid: email.uid,
|
||||||
|
subject: email.subject,
|
||||||
|
fromAddress: email.fromAddress,
|
||||||
|
fromName: email.fromName,
|
||||||
|
toAddresses: JSON.stringify(email.toAddresses),
|
||||||
|
ccAddresses: email.ccAddresses.length > 0 ? JSON.stringify(email.ccAddresses) : null,
|
||||||
|
receivedAt: email.date,
|
||||||
|
textBody: email.textBody,
|
||||||
|
htmlBody: email.htmlBody,
|
||||||
|
hasAttachments: email.hasAttachments,
|
||||||
|
attachmentNames: email.attachmentNames.length > 0
|
||||||
|
? JSON.stringify(email.attachmentNames)
|
||||||
|
: null,
|
||||||
|
isRead: folder === EmailFolder.SENT, // Gesendete E-Mails sind bereits gelesen
|
||||||
|
isStarred: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Duplikat-Fehler ignorieren (Race Condition)
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CRUD FUNCTIONS ====================
|
||||||
|
|
||||||
|
// E-Mails abrufen mit Optionen
|
||||||
|
export async function getCachedEmails(
|
||||||
|
options: EmailListOptions
|
||||||
|
): Promise<CachedEmailWithRelations[]> {
|
||||||
|
const where: Prisma.CachedEmailWhereInput = {
|
||||||
|
isDeleted: false, // Gelöschte E-Mails ausschließen
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.stressfreiEmailId) {
|
||||||
|
where.stressfreiEmailId = options.stressfreiEmailId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.customerId) {
|
||||||
|
where.stressfreiEmail = {
|
||||||
|
customerId: options.customerId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.contractId) {
|
||||||
|
where.contractId = options.contractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder-Filter (INBOX oder SENT)
|
||||||
|
if (options.folder === 'SENT') {
|
||||||
|
where.folder = EmailFolder.SENT;
|
||||||
|
} else if (options.folder === 'INBOX' || !options.folder) {
|
||||||
|
// Standard: Nur Posteingang
|
||||||
|
where.folder = EmailFolder.INBOX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body-Felder nur wenn explizit angefordert (spart Bandbreite)
|
||||||
|
const select: Prisma.CachedEmailSelect = {
|
||||||
|
id: true,
|
||||||
|
stressfreiEmailId: true,
|
||||||
|
folder: true,
|
||||||
|
messageId: true,
|
||||||
|
uid: true,
|
||||||
|
subject: true,
|
||||||
|
fromAddress: true,
|
||||||
|
fromName: true,
|
||||||
|
toAddresses: true,
|
||||||
|
ccAddresses: true,
|
||||||
|
receivedAt: true,
|
||||||
|
hasAttachments: true,
|
||||||
|
attachmentNames: true,
|
||||||
|
contractId: true,
|
||||||
|
assignedAt: true,
|
||||||
|
assignedBy: true,
|
||||||
|
isAutoAssigned: true,
|
||||||
|
isRead: true,
|
||||||
|
isStarred: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.includeBody) {
|
||||||
|
select.textBody = true;
|
||||||
|
select.htmlBody = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emails = await prisma.cachedEmail.findMany({
|
||||||
|
where,
|
||||||
|
select,
|
||||||
|
orderBy: { receivedAt: 'desc' },
|
||||||
|
take: options.limit || 50,
|
||||||
|
skip: options.offset || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return emails as CachedEmailWithRelations[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Einzelne E-Mail abrufen (mit Body)
|
||||||
|
export async function getCachedEmailById(
|
||||||
|
id: number
|
||||||
|
): Promise<CachedEmailWithRelations | null> {
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return email as CachedEmailWithRelations | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail als gelesen markieren
|
||||||
|
export async function markEmailAsRead(id: number): Promise<void> {
|
||||||
|
await prisma.cachedEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isRead: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail als ungelesen markieren
|
||||||
|
export async function markEmailAsUnread(id: number): Promise<void> {
|
||||||
|
await prisma.cachedEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isRead: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail Stern setzen/entfernen
|
||||||
|
export async function toggleEmailStar(id: number): Promise<boolean> {
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { isStarred: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Error('E-Mail nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = !email.isStarred;
|
||||||
|
await prisma.cachedEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { isStarred: newValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CONTRACT ASSIGNMENT ====================
|
||||||
|
|
||||||
|
// E-Mail einem Vertrag zuordnen
|
||||||
|
export async function assignEmailToContract(
|
||||||
|
emailId: number,
|
||||||
|
contractId: number,
|
||||||
|
userId?: number
|
||||||
|
): Promise<CachedEmailWithRelations> {
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id: emailId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Error('E-Mail nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob Vertrag existiert
|
||||||
|
const contract = await prisma.contract.findUnique({
|
||||||
|
where: { id: contractId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Vertrag nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zuordnung setzen (manuell)
|
||||||
|
const updated = await prisma.cachedEmail.update({
|
||||||
|
where: { id: emailId },
|
||||||
|
data: {
|
||||||
|
contractId,
|
||||||
|
assignedAt: new Date(),
|
||||||
|
assignedBy: userId || null,
|
||||||
|
isAutoAssigned: false, // Manuell zugeordnet
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated as CachedEmailWithRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertragszuordnung aufheben
|
||||||
|
export async function unassignEmailFromContract(
|
||||||
|
emailId: number
|
||||||
|
): Promise<CachedEmailWithRelations> {
|
||||||
|
const updated = await prisma.cachedEmail.update({
|
||||||
|
where: { id: emailId },
|
||||||
|
data: {
|
||||||
|
contractId: null,
|
||||||
|
assignedAt: null,
|
||||||
|
assignedBy: null,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated as CachedEmailWithRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== HELPER FUNCTIONS ====================
|
||||||
|
|
||||||
|
// Anzahl ungelesener E-Mails für Kunde
|
||||||
|
export async function getUnreadCountForCustomer(customerId: number): Promise<number> {
|
||||||
|
return prisma.cachedEmail.count({
|
||||||
|
where: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anzahl ungelesener E-Mails für Vertrag
|
||||||
|
export async function getUnreadCountForContract(contractId: number): Promise<number> {
|
||||||
|
return prisma.cachedEmail.count({
|
||||||
|
where: {
|
||||||
|
contractId,
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Anzahl pro Ordner für ein Konto (total und ungelesen)
|
||||||
|
export async function getFolderCountsForAccount(stressfreiEmailId: number): Promise<{
|
||||||
|
inbox: number;
|
||||||
|
inboxUnread: number;
|
||||||
|
sent: number;
|
||||||
|
sentUnread: number;
|
||||||
|
trash: number;
|
||||||
|
trashUnread: number;
|
||||||
|
}> {
|
||||||
|
const [inbox, inboxUnread, sent, sentUnread, trash, trashUnread] = await Promise.all([
|
||||||
|
// INBOX total
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false },
|
||||||
|
}),
|
||||||
|
// INBOX unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false, isRead: false },
|
||||||
|
}),
|
||||||
|
// SENT total
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false },
|
||||||
|
}),
|
||||||
|
// SENT unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
||||||
|
}),
|
||||||
|
// TRASH total (isDeleted = true)
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, isDeleted: true },
|
||||||
|
}),
|
||||||
|
// TRASH unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { stressfreiEmailId, isDeleted: true, isRead: false },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { inbox, inboxUnread, sent, sentUnread, trash, trashUnread };
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Anzahl pro Ordner für einen Vertrag (zugeordnete E-Mails)
|
||||||
|
export async function getFolderCountsForContract(contractId: number): Promise<{
|
||||||
|
inbox: number;
|
||||||
|
inboxUnread: number;
|
||||||
|
sent: number;
|
||||||
|
sentUnread: number;
|
||||||
|
}> {
|
||||||
|
const [inbox, inboxUnread, sent, sentUnread] = await Promise.all([
|
||||||
|
// INBOX total
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { contractId, folder: EmailFolder.INBOX, isDeleted: false },
|
||||||
|
}),
|
||||||
|
// INBOX unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { contractId, folder: EmailFolder.INBOX, isDeleted: false, isRead: false },
|
||||||
|
}),
|
||||||
|
// SENT total
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { contractId, folder: EmailFolder.SENT, isDeleted: false },
|
||||||
|
}),
|
||||||
|
// SENT unread
|
||||||
|
prisma.cachedEmail.count({
|
||||||
|
where: { contractId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { inbox, inboxUnread, sent, sentUnread };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle StressfreiEmails eines Kunden mit Mailbox
|
||||||
|
export async function getMailboxAccountsForCustomer(customerId: number) {
|
||||||
|
return prisma.stressfreiEmail.findMany({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
hasMailbox: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
notes: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
cachedEmails: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { email: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail-Thread finden (basierend auf References/In-Reply-To)
|
||||||
|
export async function getEmailThread(emailId: number): Promise<CachedEmailWithRelations[]> {
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id: emailId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suche nach E-Mails mit dem gleichen Betreff (vereinfachte Thread-Logik)
|
||||||
|
// In einer vollständigen Implementierung würde man References/In-Reply-To parsen
|
||||||
|
const baseSubject = email.subject?.replace(/^(Re|Fwd|Aw|Wg):\s*/gi, '') || '';
|
||||||
|
|
||||||
|
if (!baseSubject) {
|
||||||
|
return [email as CachedEmailWithRelations];
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = await prisma.cachedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
stressfreiEmailId: email.stressfreiEmailId,
|
||||||
|
OR: [
|
||||||
|
{ subject: baseSubject },
|
||||||
|
{ subject: { startsWith: `Re: ${baseSubject}` } },
|
||||||
|
{ subject: { startsWith: `Aw: ${baseSubject}` } },
|
||||||
|
{ subject: { startsWith: `Fwd: ${baseSubject}` } },
|
||||||
|
{ subject: { startsWith: `Wg: ${baseSubject}` } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { receivedAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return thread as CachedEmailWithRelations[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TRASH OPERATIONS ====================
|
||||||
|
|
||||||
|
export interface TrashOperationResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail in Papierkorb verschieben (markiert als gelöscht + IMAP-Move)
|
||||||
|
export async function moveEmailToTrash(id: number): Promise<TrashOperationResult> {
|
||||||
|
// E-Mail mit StressfreiEmail laden
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email.isDeleted) {
|
||||||
|
return { success: false, error: 'E-Mail ist bereits im Papierkorb' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP-Einstellungen und Credentials laden
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let password: string;
|
||||||
|
try {
|
||||||
|
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: email.stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ordner bestimmen (INBOX oder Sent)
|
||||||
|
const sourceFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||||
|
|
||||||
|
// Auf IMAP-Server in Trash verschieben
|
||||||
|
const imapResult = await moveToTrash(credentials, email.uid, sourceFolder);
|
||||||
|
|
||||||
|
if (!imapResult.success) {
|
||||||
|
return { success: false, error: imapResult.error || 'IMAP-Fehler beim Verschieben' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// In DB als gelöscht markieren (mit neuer UID im Trash)
|
||||||
|
await prisma.cachedEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
isDeleted: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
uid: imapResult.newUid || email.uid, // Neue UID im Trash speichern
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail aus Papierkorb wiederherstellen
|
||||||
|
export async function restoreEmailFromTrash(id: number): Promise<TrashOperationResult> {
|
||||||
|
// E-Mail laden
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.isDeleted) {
|
||||||
|
return { success: false, error: 'E-Mail ist nicht im Papierkorb' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP-Einstellungen und Credentials
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let password: string;
|
||||||
|
try {
|
||||||
|
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: email.stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ziel-Ordner bestimmen (basierend auf originalem Ordner)
|
||||||
|
const targetFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||||
|
|
||||||
|
// Auf IMAP-Server aus Trash wiederherstellen
|
||||||
|
const imapResult = await restoreFromTrash(credentials, email.uid, targetFolder);
|
||||||
|
|
||||||
|
if (!imapResult.success) {
|
||||||
|
return { success: false, error: imapResult.error || 'IMAP-Fehler beim Wiederherstellen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// In DB als wiederhergestellt markieren
|
||||||
|
await prisma.cachedEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
isDeleted: false,
|
||||||
|
deletedAt: null,
|
||||||
|
uid: imapResult.newUid || email.uid, // Neue UID im wiederhergestellten Ordner
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail endgültig löschen (aus Papierkorb)
|
||||||
|
export async function permanentDeleteEmail(id: number): Promise<TrashOperationResult> {
|
||||||
|
// E-Mail laden
|
||||||
|
const email = await prisma.cachedEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.isDeleted) {
|
||||||
|
return { success: false, error: 'E-Mail muss erst in den Papierkorb verschoben werden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP-Einstellungen und Credentials
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let password: string;
|
||||||
|
try {
|
||||||
|
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: email.stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auf IMAP-Server endgültig löschen (aus Trash)
|
||||||
|
const imapResult = await permanentDelete(credentials, email.uid);
|
||||||
|
|
||||||
|
if (!imapResult.success) {
|
||||||
|
return { success: false, error: imapResult.error || 'IMAP-Fehler beim endgültigen Löschen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aus DB löschen
|
||||||
|
await prisma.cachedEmail.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Papierkorb-E-Mails für einen Kunden abrufen
|
||||||
|
export async function getTrashEmails(customerId: number): Promise<CachedEmailWithRelations[]> {
|
||||||
|
return prisma.cachedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
isDeleted: true,
|
||||||
|
stressfreiEmail: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
stressfreiEmail: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { deletedAt: 'desc' },
|
||||||
|
}) as Promise<CachedEmailWithRelations[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Papierkorb-E-Mails zählen
|
||||||
|
export async function getTrashCount(customerId: number): Promise<number> {
|
||||||
|
return prisma.cachedEmail.count({
|
||||||
|
where: {
|
||||||
|
isDeleted: true,
|
||||||
|
stressfreiEmail: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: E-Mail löschen (jetzt deprecated, nutze moveEmailToTrash)
|
||||||
|
export async function deleteCachedEmail(id: number): Promise<void> {
|
||||||
|
// Zur Abwärtskompatibilität: In Papierkorb verschieben statt löschen
|
||||||
|
const result = await moveEmailToTrash(id);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Fehler beim Löschen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle gecachten E-Mails für eine StressfreiEmail löschen (nur Cache, kein IMAP)
|
||||||
|
export async function clearCacheForAccount(stressfreiEmailId: number): Promise<number> {
|
||||||
|
const result = await prisma.cachedEmail.deleteMany({
|
||||||
|
where: { stressfreiEmailId },
|
||||||
|
});
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SENT EMAILS ====================
|
||||||
|
|
||||||
|
// Gesendete E-Mail speichern
|
||||||
|
export async function createSentEmail(
|
||||||
|
stressfreiEmailId: number,
|
||||||
|
params: SentEmailParams
|
||||||
|
): Promise<CachedEmail> {
|
||||||
|
// StressfreiEmail laden um Absender-Info zu bekommen
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id: stressfreiEmailId },
|
||||||
|
select: { email: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
throw new Error('StressfreiEmail nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAttachments = params.attachmentNames && params.attachmentNames.length > 0;
|
||||||
|
|
||||||
|
const email = await prisma.cachedEmail.create({
|
||||||
|
data: {
|
||||||
|
stressfreiEmailId,
|
||||||
|
folder: EmailFolder.SENT,
|
||||||
|
messageId: params.messageId,
|
||||||
|
uid: params.uid || 0, // UID vom IMAP Sent-Ordner (für Attachment-Download)
|
||||||
|
subject: params.subject || null,
|
||||||
|
fromAddress: stressfreiEmail.email,
|
||||||
|
fromName: null,
|
||||||
|
toAddresses: JSON.stringify(params.to),
|
||||||
|
ccAddresses: params.cc && params.cc.length > 0 ? JSON.stringify(params.cc) : null,
|
||||||
|
receivedAt: new Date(), // Sendezeitpunkt
|
||||||
|
textBody: params.text || null,
|
||||||
|
htmlBody: params.html || null,
|
||||||
|
hasAttachments: hasAttachments || false,
|
||||||
|
attachmentNames: hasAttachments ? JSON.stringify(params.attachmentNames) : null,
|
||||||
|
isRead: true, // Gesendete sind immer "gelesen"
|
||||||
|
isStarred: false,
|
||||||
|
// Vertragszuordnung falls angegeben (automatisch = aus Vertrag gesendet)
|
||||||
|
contractId: params.contractId || null,
|
||||||
|
assignedAt: params.contractId ? new Date() : null,
|
||||||
|
isAutoAssigned: params.contractId ? true : false, // Automatisch wenn aus Vertrag gesendet
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anzahl E-Mails für Konto nach Ordner
|
||||||
|
export async function getEmailCountByFolder(
|
||||||
|
stressfreiEmailId: number,
|
||||||
|
folder: 'INBOX' | 'SENT'
|
||||||
|
): Promise<number> {
|
||||||
|
return prisma.cachedEmail.count({
|
||||||
|
where: {
|
||||||
|
stressfreiEmailId,
|
||||||
|
folder: folder === 'SENT' ? EmailFolder.SENT : EmailFolder.INBOX,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
EmailExistsResult,
|
EmailExistsResult,
|
||||||
EmailOperationResult,
|
EmailOperationResult,
|
||||||
CreateEmailParams,
|
CreateEmailParams,
|
||||||
|
MailEncryption,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { PleskEmailProvider } from './pleskProvider.js';
|
import { PleskEmailProvider } from './pleskProvider.js';
|
||||||
|
|
||||||
@@ -68,6 +69,10 @@ export interface CreateProviderConfigData {
|
|||||||
password?: string;
|
password?: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail?: string;
|
defaultForwardEmail?: string;
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
imapEncryption?: MailEncryption;
|
||||||
|
smtpEncryption?: MailEncryption;
|
||||||
|
allowSelfSignedCerts?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
@@ -95,6 +100,9 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
|||||||
passwordEncrypted,
|
passwordEncrypted,
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
defaultForwardEmail: data.defaultForwardEmail || null,
|
defaultForwardEmail: data.defaultForwardEmail || null,
|
||||||
|
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||||
|
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||||
|
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||||
isActive: data.isActive ?? true,
|
isActive: data.isActive ?? true,
|
||||||
isDefault: data.isDefault ?? false,
|
isDefault: data.isDefault ?? false,
|
||||||
},
|
},
|
||||||
@@ -123,6 +131,9 @@ export async function updateProviderConfig(
|
|||||||
if (data.domain !== undefined) updateData.domain = data.domain;
|
if (data.domain !== undefined) updateData.domain = data.domain;
|
||||||
if (data.defaultForwardEmail !== undefined)
|
if (data.defaultForwardEmail !== undefined)
|
||||||
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
|
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
|
||||||
|
if (data.imapEncryption !== undefined) updateData.imapEncryption = data.imapEncryption;
|
||||||
|
if (data.smtpEncryption !== undefined) updateData.smtpEncryption = data.smtpEncryption;
|
||||||
|
if (data.allowSelfSignedCerts !== undefined) updateData.allowSelfSignedCerts = data.allowSelfSignedCerts;
|
||||||
if (data.isActive !== undefined) updateData.isActive = data.isActive;
|
if (data.isActive !== undefined) updateData.isActive = data.isActive;
|
||||||
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
||||||
|
|
||||||
@@ -179,6 +190,13 @@ async function getProviderInstance(): Promise<IEmailProvider> {
|
|||||||
password,
|
password,
|
||||||
domain: dbConfig.domain,
|
domain: dbConfig.domain,
|
||||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||||
|
imapServer: dbConfig.imapServer || undefined,
|
||||||
|
imapPort: dbConfig.imapPort || undefined,
|
||||||
|
smtpServer: dbConfig.smtpServer || undefined,
|
||||||
|
smtpPort: dbConfig.smtpPort || undefined,
|
||||||
|
imapEncryption: dbConfig.imapEncryption as MailEncryption,
|
||||||
|
smtpEncryption: dbConfig.smtpEncryption as MailEncryption,
|
||||||
|
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
|
||||||
isActive: dbConfig.isActive,
|
isActive: dbConfig.isActive,
|
||||||
isDefault: dbConfig.isDefault,
|
isDefault: dbConfig.isDefault,
|
||||||
};
|
};
|
||||||
@@ -239,6 +257,169 @@ export async function provisionEmail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E-Mail mit echter Mailbox erstellen (IMAP/SMTP-Zugang)
|
||||||
|
export async function provisionEmailWithMailbox(
|
||||||
|
localPart: string,
|
||||||
|
customerEmail: string,
|
||||||
|
password: string
|
||||||
|
): Promise<EmailOperationResult & { email?: string }> {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
const config = await getActiveProviderConfig();
|
||||||
|
|
||||||
|
// Weiterleitungsziele zusammenstellen
|
||||||
|
const forwardTargets: string[] = [customerEmail];
|
||||||
|
|
||||||
|
// Unsere eigene Weiterleitungsadresse hinzufügen falls konfiguriert
|
||||||
|
if (config?.defaultForwardEmail) {
|
||||||
|
forwardTargets.push(config.defaultForwardEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob existiert
|
||||||
|
const exists = await provider.emailExists(localPart);
|
||||||
|
if (exists.exists) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `E-Mail ${exists.email} existiert bereits`,
|
||||||
|
email: exists.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mit Mailbox erstellen
|
||||||
|
const result = await provider.createEmailWithMailbox({
|
||||||
|
localPart,
|
||||||
|
forwardTargets,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox für existierende E-Mail-Weiterleitung aktivieren
|
||||||
|
export async function enableMailboxForExistingEmail(
|
||||||
|
localPart: string,
|
||||||
|
password: string
|
||||||
|
): Promise<EmailOperationResult> {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
|
||||||
|
const result = await provider.enableMailboxForExisting({
|
||||||
|
localPart,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox-Passwort beim Provider aktualisieren
|
||||||
|
export async function updateMailboxPassword(
|
||||||
|
localPart: string,
|
||||||
|
password: string
|
||||||
|
): Promise<EmailOperationResult> {
|
||||||
|
try {
|
||||||
|
const provider = await getProviderInstance();
|
||||||
|
|
||||||
|
const result = await provider.updateMailboxPassword({
|
||||||
|
localPart,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP/SMTP-Einstellungen vom aktiven Provider holen
|
||||||
|
export interface ImapSmtpSettings {
|
||||||
|
imapServer: string;
|
||||||
|
imapPort: number;
|
||||||
|
imapEncryption: MailEncryption; // SSL, STARTTLS oder NONE
|
||||||
|
smtpServer: string;
|
||||||
|
smtpPort: number;
|
||||||
|
smtpEncryption: MailEncryption; // SSL, STARTTLS oder NONE
|
||||||
|
allowSelfSignedCerts: boolean; // Selbstsignierte Zertifikate erlauben
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImapSmtpSettings(): Promise<ImapSmtpSettings | null> {
|
||||||
|
const config = await getActiveProviderConfig();
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
// Default-Server: Hostname aus der apiUrl extrahieren (z.B. rs001871.fastrootserver.de aus https://rs001871.fastrootserver.de:8443)
|
||||||
|
// Der Plesk-Server ist gleichzeitig der Mail-Server
|
||||||
|
let defaultServer: string;
|
||||||
|
try {
|
||||||
|
const url = new URL(config.apiUrl);
|
||||||
|
defaultServer = url.hostname;
|
||||||
|
} catch {
|
||||||
|
// Fallback falls apiUrl ungültig
|
||||||
|
defaultServer = `mail.${config.domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
const imapEncryption = (config.imapEncryption ?? 'SSL') as MailEncryption;
|
||||||
|
const smtpEncryption = (config.smtpEncryption ?? 'SSL') as MailEncryption;
|
||||||
|
|
||||||
|
// Ports basierend auf Verschlüsselung berechnen:
|
||||||
|
// SSL: IMAP 993, SMTP 465
|
||||||
|
// STARTTLS: IMAP 143, SMTP 587
|
||||||
|
// NONE: IMAP 143, SMTP 25
|
||||||
|
//
|
||||||
|
// Standard-Ports werden IMMER basierend auf Verschlüsselung berechnet.
|
||||||
|
// Nur benutzerdefinierte Ports (nicht 993/143/465/587/25) werden aus der DB übernommen.
|
||||||
|
const getImapPort = (enc: MailEncryption, storedPort: number | null) => {
|
||||||
|
const standardPorts = [993, 143];
|
||||||
|
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
|
||||||
|
if (storedPort && !standardPorts.includes(storedPort)) {
|
||||||
|
return storedPort;
|
||||||
|
}
|
||||||
|
// Sonst basierend auf Verschlüsselung
|
||||||
|
return enc === 'SSL' ? 993 : 143;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSmtpPort = (enc: MailEncryption, storedPort: number | null) => {
|
||||||
|
const standardPorts = [465, 587, 25];
|
||||||
|
// Wenn ein nicht-standard Port gespeichert ist, diesen verwenden
|
||||||
|
if (storedPort && !standardPorts.includes(storedPort)) {
|
||||||
|
return storedPort;
|
||||||
|
}
|
||||||
|
// Sonst basierend auf Verschlüsselung
|
||||||
|
if (enc === 'SSL') return 465;
|
||||||
|
if (enc === 'STARTTLS') return 587;
|
||||||
|
return 25; // NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
imapServer: config.imapServer || defaultServer,
|
||||||
|
imapPort: getImapPort(imapEncryption, config.imapPort),
|
||||||
|
imapEncryption,
|
||||||
|
smtpServer: config.smtpServer || defaultServer,
|
||||||
|
smtpPort: getSmtpPort(smtpEncryption, config.smtpPort),
|
||||||
|
smtpEncryption,
|
||||||
|
allowSelfSignedCerts: config.allowSelfSignedCerts ?? false,
|
||||||
|
domain: config.domain,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// E-Mail löschen
|
// E-Mail löschen
|
||||||
export async function deprovisionEmail(localPart: string): Promise<EmailOperationResult> {
|
export async function deprovisionEmail(localPart: string): Promise<EmailOperationResult> {
|
||||||
try {
|
try {
|
||||||
@@ -328,6 +509,13 @@ async function getProviderInstanceById(id: number): Promise<IEmailProvider> {
|
|||||||
password,
|
password,
|
||||||
domain: dbConfig.domain,
|
domain: dbConfig.domain,
|
||||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||||
|
imapServer: dbConfig.imapServer || undefined,
|
||||||
|
imapPort: dbConfig.imapPort || undefined,
|
||||||
|
smtpServer: dbConfig.smtpServer || undefined,
|
||||||
|
smtpPort: dbConfig.smtpPort || undefined,
|
||||||
|
imapEncryption: dbConfig.imapEncryption as MailEncryption,
|
||||||
|
smtpEncryption: dbConfig.smtpEncryption as MailEncryption,
|
||||||
|
allowSelfSignedCerts: dbConfig.allowSelfSignedCerts,
|
||||||
isActive: dbConfig.isActive,
|
isActive: dbConfig.isActive,
|
||||||
isDefault: dbConfig.isDefault,
|
isDefault: dbConfig.isDefault,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
EmailExistsResult,
|
EmailExistsResult,
|
||||||
EmailOperationResult,
|
EmailOperationResult,
|
||||||
CreateEmailParams,
|
CreateEmailParams,
|
||||||
|
CreateEmailWithMailboxParams,
|
||||||
|
CreateEmailWithMailboxResult,
|
||||||
|
EnableMailboxParams,
|
||||||
|
UpdateMailboxPasswordParams,
|
||||||
RenameEmailParams,
|
RenameEmailParams,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
@@ -173,9 +177,20 @@ export class PleskEmailProvider implements IEmailProvider {
|
|||||||
|
|
||||||
// stdout sollte die Mail-Infos enthalten
|
// stdout sollte die Mail-Infos enthalten
|
||||||
const exists = result.stdout?.toLowerCase().includes(localPart.toLowerCase());
|
const exists = result.stdout?.toLowerCase().includes(localPart.toLowerCase());
|
||||||
|
|
||||||
|
// Mailbox-Status aus stdout parsen (Format: "Mailbox: true" oder "Mailbox: false")
|
||||||
|
let hasMailbox: boolean | undefined;
|
||||||
|
if (exists && result.stdout) {
|
||||||
|
const mailboxMatch = result.stdout.match(/Mailbox:\s*(true|false)/i);
|
||||||
|
if (mailboxMatch) {
|
||||||
|
hasMailbox = mailboxMatch[1].toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exists,
|
exists,
|
||||||
email: exists ? email : undefined,
|
email: exists ? email : undefined,
|
||||||
|
hasMailbox,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// HTTP-Fehler oder Netzwerkfehler
|
// HTTP-Fehler oder Netzwerkfehler
|
||||||
@@ -231,6 +246,127 @@ export class PleskEmailProvider implements IEmailProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createEmailWithMailbox(params: CreateEmailWithMailboxParams): Promise<CreateEmailWithMailboxResult> {
|
||||||
|
const { localPart, forwardTargets, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfen ob schon existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} existiert bereits`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plesk CLI API: Mail-Account mit echter Mailbox erstellen
|
||||||
|
// -mailbox true: Echte Mailbox (IMAP/SMTP-Zugang)
|
||||||
|
// -passwd: Passwort für die Mailbox
|
||||||
|
// -forwarding true: Zusätzlich Weiterleitung aktivieren
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--create', email,
|
||||||
|
'-mailbox', 'true',
|
||||||
|
'-passwd', password,
|
||||||
|
'-forwarding', 'true',
|
||||||
|
'-forwarding-addresses', `add:${forwardTargets.join(',')}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `E-Mail ${email} mit Mailbox erfolgreich erstellt`,
|
||||||
|
email,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk createEmailWithMailbox error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Erstellen der E-Mail mit Mailbox: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableMailboxForExisting(params: EnableMailboxParams): Promise<EmailOperationResult> {
|
||||||
|
const { localPart, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (!exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} nicht gefunden`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plesk CLI API: Mailbox für existierende E-Mail aktivieren
|
||||||
|
// --update: Existierende E-Mail aktualisieren
|
||||||
|
// -mailbox true: Mailbox aktivieren
|
||||||
|
// -passwd: Passwort für die Mailbox setzen
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--update', email,
|
||||||
|
'-mailbox', 'true',
|
||||||
|
'-passwd', password,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Mailbox für ${email} erfolgreich aktiviert`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk enableMailboxForExisting error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Aktivieren der Mailbox: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMailboxPassword(params: UpdateMailboxPasswordParams): Promise<EmailOperationResult> {
|
||||||
|
const { localPart, password } = params;
|
||||||
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prüfen ob E-Mail existiert
|
||||||
|
const exists = await this.emailExists(localPart);
|
||||||
|
if (!exists.exists) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `E-Mail ${email} nicht gefunden`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plesk CLI API: Passwort für existierende E-Mail aktualisieren
|
||||||
|
// --update: Existierende E-Mail aktualisieren
|
||||||
|
// -passwd: Neues Passwort setzen
|
||||||
|
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||||
|
params: [
|
||||||
|
'--update', email,
|
||||||
|
'-passwd', password,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Passwort für ${email} erfolgreich aktualisiert`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
console.error('Plesk updateMailboxPassword error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Aktualisieren des Passworts: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async deleteEmail(localPart: string): Promise<EmailOperationResult> {
|
async deleteEmail(localPart: string): Promise<EmailOperationResult> {
|
||||||
const email = `${localPart}@${this.config.domain}`;
|
const email = `${localPart}@${this.config.domain}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// ==================== EMAIL PROVIDER TYPES ====================
|
// ==================== EMAIL PROVIDER TYPES ====================
|
||||||
|
|
||||||
|
// Verschlüsselungstyp für E-Mail-Verbindungen
|
||||||
|
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
|
||||||
export interface EmailForwardTarget {
|
export interface EmailForwardTarget {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
@@ -9,6 +12,27 @@ export interface CreateEmailParams {
|
|||||||
forwardTargets: string[]; // Weiterleitungsziele
|
forwardTargets: string[]; // Weiterleitungsziele
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateEmailWithMailboxParams {
|
||||||
|
localPart: string; // z.B. "max.mustermann"
|
||||||
|
forwardTargets: string[]; // Weiterleitungsziele
|
||||||
|
password: string; // Passwort für Mailbox (IMAP/SMTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEmailWithMailboxResult extends EmailOperationResult {
|
||||||
|
// Erfolg: Mailbox-Informationen zurückgeben
|
||||||
|
email?: string; // Vollständige E-Mail-Adresse
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnableMailboxParams {
|
||||||
|
localPart: string; // z.B. "max.mustermann"
|
||||||
|
password: string; // Passwort für Mailbox (IMAP/SMTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMailboxPasswordParams {
|
||||||
|
localPart: string; // z.B. "max.mustermann"
|
||||||
|
password: string; // Neues Passwort für Mailbox
|
||||||
|
}
|
||||||
|
|
||||||
export interface RenameEmailParams {
|
export interface RenameEmailParams {
|
||||||
oldLocalPart: string;
|
oldLocalPart: string;
|
||||||
newLocalPart: string;
|
newLocalPart: string;
|
||||||
@@ -17,6 +41,7 @@ export interface RenameEmailParams {
|
|||||||
export interface EmailExistsResult {
|
export interface EmailExistsResult {
|
||||||
exists: boolean;
|
exists: boolean;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
hasMailbox?: boolean; // true wenn echte Mailbox vorhanden
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailOperationResult {
|
export interface EmailOperationResult {
|
||||||
@@ -36,9 +61,18 @@ export interface IEmailProvider {
|
|||||||
// Prüft ob eine E-Mail-Adresse existiert
|
// Prüft ob eine E-Mail-Adresse existiert
|
||||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||||
|
|
||||||
// Erstellt eine neue E-Mail-Weiterleitung
|
// Erstellt eine neue E-Mail-Weiterleitung (ohne Mailbox)
|
||||||
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
||||||
|
|
||||||
|
// Erstellt eine neue E-Mail mit echter Mailbox (IMAP/SMTP-Zugang)
|
||||||
|
createEmailWithMailbox(params: CreateEmailWithMailboxParams): Promise<CreateEmailWithMailboxResult>;
|
||||||
|
|
||||||
|
// Aktiviert Mailbox für eine existierende E-Mail-Weiterleitung
|
||||||
|
enableMailboxForExisting(params: EnableMailboxParams): Promise<EmailOperationResult>;
|
||||||
|
|
||||||
|
// Aktualisiert das Passwort einer Mailbox
|
||||||
|
updateMailboxPassword(params: UpdateMailboxPasswordParams): Promise<EmailOperationResult>;
|
||||||
|
|
||||||
// Löscht eine E-Mail-Adresse
|
// Löscht eine E-Mail-Adresse
|
||||||
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
||||||
|
|
||||||
@@ -60,6 +94,15 @@ export interface EmailProviderConfig {
|
|||||||
password?: string; // Entschlüsselt
|
password?: string; // Entschlüsselt
|
||||||
domain: string;
|
domain: string;
|
||||||
defaultForwardEmail?: string;
|
defaultForwardEmail?: string;
|
||||||
|
// IMAP/SMTP-Server für E-Mail-Client (optional, default: mail.{domain})
|
||||||
|
imapServer?: string;
|
||||||
|
imapPort?: number;
|
||||||
|
smtpServer?: string;
|
||||||
|
smtpPort?: number;
|
||||||
|
// Verschlüsselungs-Einstellungen
|
||||||
|
imapEncryption?: MailEncryption; // SSL, STARTTLS oder NONE
|
||||||
|
smtpEncryption?: MailEncryption; // SSL, STARTTLS oder NONE
|
||||||
|
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,825 @@
|
|||||||
|
// ==================== IMAP SERVICE ====================
|
||||||
|
// Service für IMAP-Zugriff auf Mailboxen
|
||||||
|
|
||||||
|
import { ImapFlow, FetchMessageObject } from 'imapflow';
|
||||||
|
import { simpleParser, ParsedMail, AddressObject } from 'mailparser';
|
||||||
|
|
||||||
|
// Verschlüsselungstyp
|
||||||
|
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
|
||||||
|
export interface ImapCredentials {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
encryption?: MailEncryption; // SSL, STARTTLS oder NONE (Standard: SSL)
|
||||||
|
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchedEmail {
|
||||||
|
uid: number;
|
||||||
|
messageId: string;
|
||||||
|
subject: string | null;
|
||||||
|
fromAddress: string;
|
||||||
|
fromName: string | null;
|
||||||
|
toAddresses: string[];
|
||||||
|
ccAddresses: string[];
|
||||||
|
date: Date;
|
||||||
|
textBody: string | null;
|
||||||
|
htmlBody: string | null;
|
||||||
|
hasAttachments: boolean;
|
||||||
|
attachmentNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchOptions {
|
||||||
|
folder?: string; // Default: 'INBOX'
|
||||||
|
since?: Date; // Nur E-Mails nach diesem Datum
|
||||||
|
limit?: number; // Max. Anzahl E-Mails
|
||||||
|
sinceUid?: number; // Nur E-Mails ab dieser UID (für inkrementellen Sync)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Adressen aus mailparser-Format extrahieren
|
||||||
|
function extractAddresses(addressObj: AddressObject | AddressObject[] | undefined): string[] {
|
||||||
|
if (!addressObj) return [];
|
||||||
|
|
||||||
|
const addresses = Array.isArray(addressObj) ? addressObj : [addressObj];
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
for (const obj of addresses) {
|
||||||
|
if (obj.value) {
|
||||||
|
for (const addr of obj.value) {
|
||||||
|
if (addr.address) {
|
||||||
|
result.push(addr.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Ersten Absender-Namen extrahieren
|
||||||
|
function extractFromName(addressObj: AddressObject | AddressObject[] | undefined): string | null {
|
||||||
|
if (!addressObj) return null;
|
||||||
|
|
||||||
|
const addresses = Array.isArray(addressObj) ? addressObj : [addressObj];
|
||||||
|
|
||||||
|
for (const obj of addresses) {
|
||||||
|
if (obj.value && obj.value[0]) {
|
||||||
|
return obj.value[0].name || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mails aus einer Mailbox abrufen
|
||||||
|
export async function fetchEmails(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
options: FetchOptions = {}
|
||||||
|
): Promise<FetchedEmail[]> {
|
||||||
|
const {
|
||||||
|
folder = 'INBOX',
|
||||||
|
since,
|
||||||
|
limit = 50,
|
||||||
|
sinceUid,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
// ImapFlow-Optionen je nach Verschlüsselungstyp
|
||||||
|
// SSL: secure=true (implicit TLS, Port 993)
|
||||||
|
// STARTTLS: secure=false (upgrades to TLS, Port 143)
|
||||||
|
// NONE: secure=false + disableAutoIdle (no encryption, Port 143)
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TLS-Optionen nur wenn nicht NONE
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug-Logging
|
||||||
|
console.log(`[IMAP] Connecting to ${credentials.host}:${credentials.port} (${encryption}), user: ${credentials.user}`);
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
const emails: FetchedEmail[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log(`[IMAP] Connected successfully`);
|
||||||
|
|
||||||
|
// Mailbox öffnen
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
|
||||||
|
// Suchkriterien zusammenstellen
|
||||||
|
let searchCriteria: { since?: Date; uid?: string } = {};
|
||||||
|
|
||||||
|
if (since) {
|
||||||
|
searchCriteria.since = since;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP SEARCH ausführen
|
||||||
|
let uids: number[];
|
||||||
|
|
||||||
|
if (sinceUid) {
|
||||||
|
// Inkrementeller Sync: Nur E-Mails ab einer bestimmten UID
|
||||||
|
// IMAP UID-Range: "sinceUid:*" bedeutet alle E-Mails >= sinceUid
|
||||||
|
const messages = await client.search({ uid: `${sinceUid}:*` }, { uid: true });
|
||||||
|
const messageArray = Array.isArray(messages) ? messages : [];
|
||||||
|
uids = messageArray.filter((uid: number) => uid > sinceUid); // Exkludiere die sinceUid selbst
|
||||||
|
} else if (since) {
|
||||||
|
// Nach Datum suchen
|
||||||
|
const messages = await client.search({ since }, { uid: true });
|
||||||
|
uids = Array.isArray(messages) ? messages : [];
|
||||||
|
} else {
|
||||||
|
// Alle E-Mails (mit Limit)
|
||||||
|
const messages = await client.search({ all: true }, { uid: true });
|
||||||
|
uids = Array.isArray(messages) ? messages : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neueste zuerst (absteigend sortieren)
|
||||||
|
uids.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
// Limit anwenden
|
||||||
|
const limitedUids = uids.slice(0, limit);
|
||||||
|
|
||||||
|
console.log(`[IMAP] Found ${uids.length} emails, fetching ${limitedUids.length}`);
|
||||||
|
|
||||||
|
if (limitedUids.length === 0) {
|
||||||
|
console.log(`[IMAP] No emails to fetch`);
|
||||||
|
await client.logout();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mails abrufen
|
||||||
|
for await (const message of client.fetch(limitedUids, {
|
||||||
|
uid: true,
|
||||||
|
envelope: true,
|
||||||
|
source: true, // Vollständige E-Mail für Parsing
|
||||||
|
})) {
|
||||||
|
try {
|
||||||
|
// Source muss vorhanden sein
|
||||||
|
if (!message.source) {
|
||||||
|
console.error(`E-Mail UID ${message.uid} hat keine Source`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail mit mailparser parsen
|
||||||
|
const parsed = await simpleParser(message.source) as ParsedMail;
|
||||||
|
|
||||||
|
const email: FetchedEmail = {
|
||||||
|
uid: message.uid,
|
||||||
|
messageId: parsed.messageId || `${message.uid}@unknown`,
|
||||||
|
subject: parsed.subject || null,
|
||||||
|
fromAddress: extractAddresses(parsed.from)[0] || 'unknown@unknown',
|
||||||
|
fromName: extractFromName(parsed.from),
|
||||||
|
toAddresses: extractAddresses(parsed.to),
|
||||||
|
ccAddresses: extractAddresses(parsed.cc),
|
||||||
|
date: parsed.date || new Date(),
|
||||||
|
textBody: parsed.text || null,
|
||||||
|
htmlBody: parsed.html ? String(parsed.html) : null,
|
||||||
|
hasAttachments: (parsed.attachments?.length || 0) > 0,
|
||||||
|
attachmentNames: parsed.attachments?.map((a) => a.filename || 'unnamed') || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
emails.push(email);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error(`Fehler beim Parsen von E-Mail UID ${message.uid}:`, parseError);
|
||||||
|
// E-Mail überspringen bei Parse-Fehlern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
} catch (error) {
|
||||||
|
// Verbindung sauber schließen bei Fehlern
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bessere Fehlermeldungen
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const msg = error.message.toLowerCase();
|
||||||
|
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (msg.includes('authentication') || msg.includes('login')) {
|
||||||
|
throw new Error('IMAP-Authentifizierung fehlgeschlagen - Zugangsdaten prüfen');
|
||||||
|
}
|
||||||
|
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||||
|
throw new Error(`IMAP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
|
||||||
|
}
|
||||||
|
if (msg.includes('timeout') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||||
|
const enc = credentials.encryption ?? 'SSL';
|
||||||
|
if (enc === 'STARTTLS' && credentials.port === 143) {
|
||||||
|
throw new Error(`IMAP Port 143 (STARTTLS) nicht erreichbar auf ${credentials.host}`);
|
||||||
|
} else if (enc === 'SSL' && credentials.port === 993) {
|
||||||
|
throw new Error(`IMAP Port 993 (SSL) nicht erreichbar auf ${credentials.host}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`IMAP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Timeout`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.includes('certificate') || msg.includes('cert')) {
|
||||||
|
throw new Error('IMAP SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emails;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbindung testen
|
||||||
|
export async function testImapConnection(credentials: ImapCredentials): Promise<void> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.logout();
|
||||||
|
} catch (error) {
|
||||||
|
// Verbindung sauber schließen bei Fehlern
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const msg = error.message.toLowerCase();
|
||||||
|
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (msg.includes('authentication') || msg.includes('login')) {
|
||||||
|
throw new Error('IMAP-Authentifizierung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||||
|
throw new Error(`IMAP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
|
||||||
|
}
|
||||||
|
if (msg.includes('timeout') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||||
|
if (encryption === 'STARTTLS' && credentials.port === 143) {
|
||||||
|
throw new Error(`IMAP Port 143 (STARTTLS) nicht erreichbar auf ${credentials.host}`);
|
||||||
|
} else if (encryption === 'SSL' && credentials.port === 993) {
|
||||||
|
throw new Error(`IMAP Port 993 (SSL) nicht erreichbar auf ${credentials.host}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`IMAP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Timeout`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.includes('certificate') || msg.includes('cert')) {
|
||||||
|
throw new Error('IMAP SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Höchste UID in einer Mailbox ermitteln (für inkrementellen Sync)
|
||||||
|
export async function getHighestUid(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
folder: string = 'INBOX'
|
||||||
|
): Promise<number> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
const mailbox = await client.mailboxOpen(folder);
|
||||||
|
const highestUid = mailbox.uidNext ? mailbox.uidNext - 1 : 0;
|
||||||
|
await client.logout();
|
||||||
|
return highestUid;
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anhang-Interface
|
||||||
|
export interface EmailAttachmentData {
|
||||||
|
filename: string;
|
||||||
|
content: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anhang einer E-Mail per UID abrufen
|
||||||
|
export async function fetchAttachment(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
attachmentFilename: string,
|
||||||
|
folder: string = 'INBOX'
|
||||||
|
): Promise<EmailAttachmentData | null> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
|
||||||
|
// E-Mail per UID abrufen
|
||||||
|
let attachment: EmailAttachmentData | null = null;
|
||||||
|
|
||||||
|
for await (const message of client.fetch([uid], {
|
||||||
|
uid: true,
|
||||||
|
source: true,
|
||||||
|
})) {
|
||||||
|
if (!message.source) continue;
|
||||||
|
|
||||||
|
// E-Mail parsen
|
||||||
|
const parsed = await simpleParser(message.source);
|
||||||
|
|
||||||
|
// Anhang suchen
|
||||||
|
if (parsed.attachments) {
|
||||||
|
for (const att of parsed.attachments) {
|
||||||
|
const filename = att.filename || 'unnamed';
|
||||||
|
if (filename === attachmentFilename) {
|
||||||
|
attachment = {
|
||||||
|
filename,
|
||||||
|
content: att.content,
|
||||||
|
contentType: att.contentType || 'application/octet-stream',
|
||||||
|
size: att.size,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
return attachment;
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesendete E-Mail im IMAP Sent-Ordner speichern (für Attachment-Download)
|
||||||
|
export interface AppendToSentParams {
|
||||||
|
rawEmail: Buffer | string; // RFC 5322 formatierte E-Mail
|
||||||
|
sentFolder?: string; // Standard: 'Sent'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppendToSentResult {
|
||||||
|
success: boolean;
|
||||||
|
uid?: number; // UID der gespeicherten Nachricht
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function appendToSent(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
params: AppendToSentParams
|
||||||
|
): Promise<AppendToSentResult> {
|
||||||
|
const { rawEmail, sentFolder = 'Sent' } = params;
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[IMAP] Appending email to ${sentFolder} folder...`);
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// E-Mail im Sent-Ordner speichern
|
||||||
|
const result = await client.append(sentFolder, rawEmail, ['\\Seen'], new Date());
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
|
||||||
|
// append kann false zurückgeben bei Fehler
|
||||||
|
if (!result) {
|
||||||
|
return { success: false, error: 'IMAP append fehlgeschlagen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[IMAP] Email appended successfully, UID: ${result.uid}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
uid: result.uid,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[IMAP] Error appending to Sent folder:', error);
|
||||||
|
|
||||||
|
// Versuche mit alternativen Ordnernamen falls 'Sent' nicht existiert
|
||||||
|
if (sentFolder === 'Sent' && error instanceof Error) {
|
||||||
|
// Typische alternative Namen für Sent-Ordner
|
||||||
|
const alternativeNames = ['INBOX.Sent', 'Sent Messages', 'Sent Items'];
|
||||||
|
|
||||||
|
for (const altFolder of alternativeNames) {
|
||||||
|
try {
|
||||||
|
const altClient = new ImapFlow(clientOptions);
|
||||||
|
await altClient.connect();
|
||||||
|
const altResult = await altClient.append(altFolder, rawEmail, ['\\Seen'], new Date());
|
||||||
|
await altClient.logout();
|
||||||
|
|
||||||
|
if (altResult) {
|
||||||
|
console.log(`[IMAP] Email appended to ${altFolder}, UID: ${altResult.uid}`);
|
||||||
|
return { success: true, uid: altResult.uid };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Nächsten Namen versuchen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Speichern im Sent-Ordner',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Anhänge einer E-Mail per UID abrufen (Metadaten)
|
||||||
|
export async function fetchAttachmentList(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
folder: string = 'INBOX'
|
||||||
|
): Promise<Array<{ filename: string; contentType: string; size: number }>> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
const attachments: Array<{ filename: string; contentType: string; size: number }> = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.mailboxOpen(folder);
|
||||||
|
|
||||||
|
for await (const message of client.fetch([uid], {
|
||||||
|
uid: true,
|
||||||
|
source: true,
|
||||||
|
})) {
|
||||||
|
if (!message.source) continue;
|
||||||
|
|
||||||
|
const parsed = await simpleParser(message.source);
|
||||||
|
|
||||||
|
if (parsed.attachments) {
|
||||||
|
for (const att of parsed.attachments) {
|
||||||
|
attachments.push({
|
||||||
|
filename: att.filename || 'unnamed',
|
||||||
|
contentType: att.contentType || 'application/octet-stream',
|
||||||
|
size: att.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
return attachments;
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TRASH OPERATIONS ====================
|
||||||
|
|
||||||
|
// Typische Namen für Trash-Ordner (verschiedene Server/Sprachen)
|
||||||
|
const TRASH_FOLDER_NAMES = ['Trash', 'INBOX.Trash', 'Deleted', 'Deleted Items', 'Deleted Messages', 'Papierkorb'];
|
||||||
|
|
||||||
|
// Helper: Trash-Ordner finden
|
||||||
|
async function findTrashFolder(client: ImapFlow): Promise<string | null> {
|
||||||
|
const mailboxes = await client.list();
|
||||||
|
|
||||||
|
for (const name of TRASH_FOLDER_NAMES) {
|
||||||
|
const found = mailboxes.find(m =>
|
||||||
|
m.path.toLowerCase() === name.toLowerCase() ||
|
||||||
|
m.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
|
if (found) return found.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suche nach Ordner mit \Trash Flag
|
||||||
|
const trashByFlag = mailboxes.find(m => m.specialUse === '\\Trash');
|
||||||
|
if (trashByFlag) return trashByFlag.path;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveToTrashResult {
|
||||||
|
success: boolean;
|
||||||
|
newUid?: number; // UID im Trash-Ordner
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail in Papierkorb verschieben
|
||||||
|
export async function moveToTrash(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
sourceFolder: string = 'INBOX'
|
||||||
|
): Promise<MoveToTrashResult> {
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Trash-Ordner finden
|
||||||
|
const trashFolder = await findTrashFolder(client);
|
||||||
|
if (!trashFolder) {
|
||||||
|
await client.logout();
|
||||||
|
return { success: false, error: 'Trash-Ordner nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source-Ordner öffnen
|
||||||
|
await client.mailboxOpen(sourceFolder);
|
||||||
|
|
||||||
|
// E-Mail verschieben (kopieren + löschen)
|
||||||
|
const moveResult = await client.messageMove([uid], trashFolder, { uid: true });
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
|
||||||
|
if (moveResult && moveResult.uidMap) {
|
||||||
|
// uidMap ist Map<number, number> - alte UID -> neue UID
|
||||||
|
const newUid = moveResult.uidMap.get(uid);
|
||||||
|
console.log(`[IMAP] Email moved to ${trashFolder}, new UID: ${newUid}`);
|
||||||
|
return { success: true, newUid };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[IMAP] Error moving to trash:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Verschieben in Papierkorb',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestoreFromTrashResult {
|
||||||
|
success: boolean;
|
||||||
|
newUid?: number; // UID im wiederhergestellten Ordner
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail aus Papierkorb wiederherstellen
|
||||||
|
export async function restoreFromTrash(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
targetFolder: string = 'INBOX'
|
||||||
|
): Promise<RestoreFromTrashResult> {
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Trash-Ordner finden
|
||||||
|
const trashFolder = await findTrashFolder(client);
|
||||||
|
if (!trashFolder) {
|
||||||
|
await client.logout();
|
||||||
|
return { success: false, error: 'Trash-Ordner nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trash-Ordner öffnen
|
||||||
|
await client.mailboxOpen(trashFolder);
|
||||||
|
|
||||||
|
// E-Mail zurück verschieben
|
||||||
|
const moveResult = await client.messageMove([uid], targetFolder, { uid: true });
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
|
||||||
|
if (moveResult && moveResult.uidMap) {
|
||||||
|
const newUid = moveResult.uidMap.get(uid);
|
||||||
|
console.log(`[IMAP] Email restored to ${targetFolder}, new UID: ${newUid}`);
|
||||||
|
return { success: true, newUid };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[IMAP] Error restoring from trash:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Wiederherstellen',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermanentDeleteResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail endgültig löschen (aus Trash-Ordner)
|
||||||
|
export async function permanentDelete(
|
||||||
|
credentials: ImapCredentials,
|
||||||
|
uid: number,
|
||||||
|
folder?: string // Optional: Ordner angeben, sonst wird Trash verwendet
|
||||||
|
): Promise<PermanentDeleteResult> {
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const clientOptions: ConstructorParameters<typeof ImapFlow>[0] = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
logger: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
clientOptions.tls = { rejectUnauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new ImapFlow(clientOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// Ordner bestimmen
|
||||||
|
let targetFolder: string | null | undefined = folder;
|
||||||
|
if (!targetFolder) {
|
||||||
|
targetFolder = await findTrashFolder(client);
|
||||||
|
if (!targetFolder) {
|
||||||
|
await client.logout();
|
||||||
|
return { success: false, error: 'Trash-Ordner nicht gefunden' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordner öffnen
|
||||||
|
await client.mailboxOpen(targetFolder);
|
||||||
|
|
||||||
|
// E-Mail als gelöscht markieren und expunge
|
||||||
|
await client.messageDelete([uid], { uid: true });
|
||||||
|
|
||||||
|
await client.logout();
|
||||||
|
|
||||||
|
console.log(`[IMAP] Email permanently deleted from ${targetFolder}, UID: ${uid}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await client.logout();
|
||||||
|
} catch {
|
||||||
|
// Ignorieren
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[IMAP] Error permanently deleting:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim endgültigen Löschen',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
// ==================== SMTP SERVICE ====================
|
||||||
|
// Service für E-Mail-Versand via SMTP
|
||||||
|
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import type { Transporter } from 'nodemailer';
|
||||||
|
import MailComposer from 'nodemailer/lib/mail-composer';
|
||||||
|
|
||||||
|
// Verschlüsselungstyp
|
||||||
|
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||||
|
|
||||||
|
export interface SmtpCredentials {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
encryption?: MailEncryption; // SSL, STARTTLS oder NONE (Standard: SSL)
|
||||||
|
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anhang-Interface
|
||||||
|
export interface EmailAttachment {
|
||||||
|
filename: string;
|
||||||
|
content: string; // Base64-kodierter Inhalt
|
||||||
|
contentType?: string; // MIME-Type (z.B. 'application/pdf')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendEmailParams {
|
||||||
|
to: string | string[];
|
||||||
|
cc?: string | string[];
|
||||||
|
subject: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
inReplyTo?: string; // Message-ID der E-Mail auf die geantwortet wird
|
||||||
|
references?: string[]; // Thread-Referenzen
|
||||||
|
attachments?: EmailAttachment[]; // Anhänge
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendEmailResult {
|
||||||
|
success: boolean;
|
||||||
|
messageId?: string;
|
||||||
|
rawEmail?: Buffer; // RFC 5322 formatierte E-Mail für IMAP-Speicherung
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
export async function sendEmail(
|
||||||
|
credentials: SmtpCredentials,
|
||||||
|
fromAddress: string,
|
||||||
|
params: SendEmailParams
|
||||||
|
): Promise<SendEmailResult> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
// Transport-Optionen je nach Verschlüsselungstyp
|
||||||
|
// SSL: secure=true (implicit TLS, Port 465)
|
||||||
|
// STARTTLS: secure=false (upgrades to TLS, Port 587)
|
||||||
|
// NONE: secure=false + ignoreTLS=true (no encryption, Port 25)
|
||||||
|
const transportOptions: nodemailer.TransportOptions & {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
auth: { user: string; pass: string };
|
||||||
|
tls?: { rejectUnauthorized: boolean };
|
||||||
|
ignoreTLS?: boolean;
|
||||||
|
requireTLS?: boolean;
|
||||||
|
connectionTimeout: number;
|
||||||
|
greetingTimeout: number;
|
||||||
|
socketTimeout: number;
|
||||||
|
} = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
connectionTimeout: 10000,
|
||||||
|
greetingTimeout: 10000,
|
||||||
|
socketTimeout: 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TLS-Optionen nur wenn nicht NONE
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
transportOptions.tls = { rejectUnauthorized };
|
||||||
|
} else {
|
||||||
|
// Keine Verschlüsselung: STARTTLS ignorieren
|
||||||
|
transportOptions.ignoreTLS = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bei STARTTLS: requireTLS erzwingen
|
||||||
|
if (encryption === 'STARTTLS') {
|
||||||
|
transportOptions.requireTLS = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug-Logging für Entwicklung
|
||||||
|
console.log(`[SMTP] Connecting to ${credentials.host}:${credentials.port} (${encryption}), user: ${credentials.user}`);
|
||||||
|
|
||||||
|
// Transporter erstellen
|
||||||
|
const transporter: Transporter = nodemailer.createTransport(transportOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// E-Mail-Optionen zusammenstellen
|
||||||
|
const mailOptions: nodemailer.SendMailOptions = {
|
||||||
|
from: fromAddress,
|
||||||
|
to: Array.isArray(params.to) ? params.to.join(', ') : params.to,
|
||||||
|
subject: params.subject,
|
||||||
|
};
|
||||||
|
|
||||||
|
// CC hinzufügen falls vorhanden
|
||||||
|
if (params.cc) {
|
||||||
|
mailOptions.cc = Array.isArray(params.cc) ? params.cc.join(', ') : params.cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body hinzufügen
|
||||||
|
if (params.html) {
|
||||||
|
mailOptions.html = params.html;
|
||||||
|
// Auch Text-Version für Clients ohne HTML-Support
|
||||||
|
mailOptions.text = params.text || stripHtml(params.html);
|
||||||
|
} else if (params.text) {
|
||||||
|
mailOptions.text = params.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Threading-Header für Antworten
|
||||||
|
if (params.inReplyTo) {
|
||||||
|
mailOptions.inReplyTo = params.inReplyTo;
|
||||||
|
}
|
||||||
|
if (params.references && params.references.length > 0) {
|
||||||
|
mailOptions.references = params.references.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anhänge hinzufügen
|
||||||
|
if (params.attachments && params.attachments.length > 0) {
|
||||||
|
mailOptions.attachments = params.attachments.map((att) => ({
|
||||||
|
filename: att.filename,
|
||||||
|
content: Buffer.from(att.content, 'base64'),
|
||||||
|
contentType: att.contentType,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// E-Mail senden
|
||||||
|
const result = await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
// Raw E-Mail für IMAP-Speicherung bauen (mit tatsächlicher Message-ID)
|
||||||
|
let rawEmail: Buffer | undefined;
|
||||||
|
try {
|
||||||
|
const composerOptions = {
|
||||||
|
...mailOptions,
|
||||||
|
messageId: result.messageId, // Tatsächliche Message-ID vom Server
|
||||||
|
};
|
||||||
|
const composer = new MailComposer(composerOptions);
|
||||||
|
rawEmail = await composer.compile().build();
|
||||||
|
} catch (compileError) {
|
||||||
|
console.error('Error compiling raw email:', compileError);
|
||||||
|
// Nicht kritisch - E-Mail wurde trotzdem gesendet
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: result.messageId,
|
||||||
|
rawEmail,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SMTP sendEmail error:', error);
|
||||||
|
|
||||||
|
// Bessere Fehlermeldungen
|
||||||
|
let errorMessage = 'Unbekannter Fehler beim E-Mail-Versand';
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const msg = error.message.toLowerCase();
|
||||||
|
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (msg.includes('authentication') || msg.includes('auth') || msg.includes('login')) {
|
||||||
|
errorMessage = 'SMTP-Authentifizierung fehlgeschlagen - Zugangsdaten prüfen';
|
||||||
|
} else if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||||
|
errorMessage = `SMTP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`;
|
||||||
|
} else if (msg.includes('greeting never received') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||||
|
// Detaillierte Fehlermeldung je nach Port/Verschlüsselung
|
||||||
|
const enc = credentials.encryption ?? 'SSL';
|
||||||
|
if (enc === 'STARTTLS' && credentials.port === 587) {
|
||||||
|
errorMessage = `SMTP-Verbindung zu Port 587 fehlgeschlagen - STARTTLS (Submission) ist möglicherweise nicht aktiviert auf ${credentials.host}`;
|
||||||
|
} else if (enc === 'NONE' && credentials.port === 25) {
|
||||||
|
errorMessage = `SMTP-Verbindung zu Port 25 fehlgeschlagen - Port möglicherweise blockiert oder nicht erreichbar auf ${credentials.host}`;
|
||||||
|
} else {
|
||||||
|
errorMessage = `SMTP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Port nicht erreichbar oder Timeout`;
|
||||||
|
}
|
||||||
|
} else if (msg.includes('timeout')) {
|
||||||
|
errorMessage = `SMTP-Verbindung: Zeitüberschreitung bei ${credentials.host}:${credentials.port}`;
|
||||||
|
} else if (msg.includes('recipient') || msg.includes('rejected')) {
|
||||||
|
errorMessage = 'Empfänger-Adresse wurde vom Server abgelehnt';
|
||||||
|
} else if (msg.includes('certificate') || msg.includes('cert')) {
|
||||||
|
errorMessage = 'SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben" in den Provider-Einstellungen';
|
||||||
|
} else if (msg.includes('socket close') || msg.includes('socket hang up') || msg.includes('econnreset') || errorCode === 'econnreset') {
|
||||||
|
// Server schließt Verbindung unerwartet - oft TLS-Problem bei STARTTLS
|
||||||
|
const enc = credentials.encryption ?? 'SSL';
|
||||||
|
if (enc === 'STARTTLS') {
|
||||||
|
errorMessage = `SMTP-Verbindung abgebrochen bei STARTTLS - Aktiviere "Selbstsignierte Zertifikate erlauben" oder verwende SSL/TLS auf Port 465`;
|
||||||
|
} else {
|
||||||
|
errorMessage = `SMTP-Verbindung unerwartet geschlossen von ${credentials.host}:${credentials.port}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// Transporter schließen
|
||||||
|
transporter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP-Verbindung testen
|
||||||
|
export async function testSmtpConnection(credentials: SmtpCredentials): Promise<void> {
|
||||||
|
// Verschlüsselungs-Einstellungen basierend auf Modus
|
||||||
|
const encryption = credentials.encryption ?? 'SSL';
|
||||||
|
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
|
||||||
|
|
||||||
|
const transportOptions: nodemailer.TransportOptions & {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
auth: { user: string; pass: string };
|
||||||
|
tls?: { rejectUnauthorized: boolean };
|
||||||
|
ignoreTLS?: boolean;
|
||||||
|
connectionTimeout: number;
|
||||||
|
greetingTimeout: number;
|
||||||
|
} = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
secure: encryption === 'SSL',
|
||||||
|
auth: {
|
||||||
|
user: credentials.user,
|
||||||
|
pass: credentials.password,
|
||||||
|
},
|
||||||
|
connectionTimeout: 10000,
|
||||||
|
greetingTimeout: 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encryption !== 'NONE') {
|
||||||
|
transportOptions.tls = { rejectUnauthorized };
|
||||||
|
} else {
|
||||||
|
transportOptions.ignoreTLS = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter: Transporter = nodemailer.createTransport(transportOptions);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verbindung verifizieren
|
||||||
|
await transporter.verify();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const msg = error.message.toLowerCase();
|
||||||
|
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || '';
|
||||||
|
|
||||||
|
if (msg.includes('authentication') || msg.includes('auth') || msg.includes('login')) {
|
||||||
|
throw new Error('SMTP-Authentifizierung fehlgeschlagen');
|
||||||
|
}
|
||||||
|
if (msg.includes('econnrefused') || errorCode === 'econnrefused') {
|
||||||
|
throw new Error(`SMTP-Server nicht erreichbar: ${credentials.host}:${credentials.port} - Verbindung verweigert`);
|
||||||
|
}
|
||||||
|
if (msg.includes('greeting never received') || msg.includes('etimedout') || errorCode === 'etimedout') {
|
||||||
|
if (encryption === 'STARTTLS' && credentials.port === 587) {
|
||||||
|
throw new Error(`SMTP Port 587 (STARTTLS/Submission) ist nicht erreichbar - In Plesk unter Tools & Settings > Mail Server Settings aktivieren`);
|
||||||
|
} else if (encryption === 'NONE' && credentials.port === 25) {
|
||||||
|
throw new Error(`SMTP Port 25 ist nicht erreichbar auf ${credentials.host}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`SMTP-Verbindung zu ${credentials.host}:${credentials.port} fehlgeschlagen - Port nicht erreichbar`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.includes('certificate') || msg.includes('cert')) {
|
||||||
|
throw new Error('SSL-Zertifikatfehler - Aktiviere "Selbstsignierte Zertifikate erlauben"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
transporter.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: HTML zu Text konvertieren (einfache Version)
|
||||||
|
function stripHtml(html: string): string {
|
||||||
|
return html
|
||||||
|
// Zeilenumbrüche für Block-Elemente
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
|
.replace(/<\/p>/gi, '\n\n')
|
||||||
|
.replace(/<\/div>/gi, '\n')
|
||||||
|
.replace(/<\/li>/gi, '\n')
|
||||||
|
// Alle HTML-Tags entfernen
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
// HTML-Entities dekodieren
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
// Mehrfache Leerzeilen reduzieren
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||||
|
import {
|
||||||
|
provisionEmail,
|
||||||
|
provisionEmailWithMailbox,
|
||||||
|
enableMailboxForExistingEmail,
|
||||||
|
checkEmailExists,
|
||||||
|
getProviderDomain,
|
||||||
|
updateMailboxPassword,
|
||||||
|
} from './emailProvider/emailProviderService.js';
|
||||||
|
import { generateSecurePassword } from '../utils/passwordGenerator.js';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@@ -13,22 +23,116 @@ export async function getEmailsByCustomerId(customerId: number, includeInactive
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mit Mailbox-Status für E-Mail-Client
|
||||||
|
export async function getEmailsWithMailboxByCustomerId(customerId: number) {
|
||||||
|
return prisma.stressfreiEmail.findMany({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
notes: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
cachedEmails: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { email: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getEmailById(id: number) {
|
export async function getEmailById(id: number) {
|
||||||
return prisma.stressfreiEmail.findUnique({
|
return prisma.stressfreiEmail.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEmail(data: {
|
// E-Mail mit Mailbox-Status laden
|
||||||
|
export async function getEmailWithMailboxById(id: number) {
|
||||||
|
return prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
customerId: true,
|
||||||
|
email: true,
|
||||||
|
platform: true,
|
||||||
|
notes: true,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEmailData {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
email: string;
|
email: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}) {
|
provisionAtProvider?: boolean;
|
||||||
|
createMailbox?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEmail(data: CreateEmailData) {
|
||||||
|
const { provisionAtProvider, createMailbox, ...emailData } = data;
|
||||||
|
|
||||||
|
// Falls beim Provider anlegen gewünscht
|
||||||
|
if (provisionAtProvider) {
|
||||||
|
// Kunde laden für Weiterleitung
|
||||||
|
const customer = await prisma.customer.findUnique({
|
||||||
|
where: { id: data.customerId },
|
||||||
|
select: { email: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customer?.email) {
|
||||||
|
throw new Error('Kunde hat keine E-Mail-Adresse für Weiterleitung');
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPart extrahieren
|
||||||
|
const localPart = data.email.split('@')[0];
|
||||||
|
|
||||||
|
if (createMailbox) {
|
||||||
|
// Mit echter Mailbox anlegen
|
||||||
|
const password = generateSecurePassword();
|
||||||
|
const result = await provisionEmailWithMailbox(localPart, customer.email, password);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Fehler beim Anlegen der Mailbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort verschlüsseln und speichern
|
||||||
|
const passwordEncrypted = encrypt(password);
|
||||||
|
|
||||||
|
return prisma.stressfreiEmail.create({
|
||||||
|
data: {
|
||||||
|
...emailData,
|
||||||
|
isActive: true,
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: passwordEncrypted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Nur Weiterleitung anlegen
|
||||||
|
const result = await provisionEmail(localPart, customer.email);
|
||||||
|
|
||||||
|
if (!result.success && !result.message?.includes('existiert bereits')) {
|
||||||
|
throw new Error(result.error || 'Fehler beim Anlegen der E-Mail');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return prisma.stressfreiEmail.create({
|
return prisma.stressfreiEmail.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...emailData,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
hasMailbox: createMailbox || false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,3 +155,136 @@ export async function updateEmail(
|
|||||||
export async function deleteEmail(id: number) {
|
export async function deleteEmail(id: number) {
|
||||||
return prisma.stressfreiEmail.delete({ where: { id } });
|
return prisma.stressfreiEmail.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mailbox nachträglich aktivieren (für existierende E-Mail-Weiterleitung)
|
||||||
|
export async function enableMailbox(id: number): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stressfreiEmail.hasMailbox) {
|
||||||
|
return { success: false, error: 'Mailbox ist bereits aktiviert' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
const password = generateSecurePassword();
|
||||||
|
|
||||||
|
// Mailbox für existierende E-Mail aktivieren (nicht neu erstellen!)
|
||||||
|
const result = await enableMailboxForExistingEmail(localPart, password);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, error: result.error || 'Fehler beim Aktivieren der Mailbox' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort verschlüsseln und speichern
|
||||||
|
const passwordEncrypted = encrypt(password);
|
||||||
|
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
hasMailbox: true,
|
||||||
|
emailPasswordEncrypted: passwordEncrypted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox-Status mit Provider synchronisieren
|
||||||
|
export async function syncMailboxStatus(id: number): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
hasMailbox?: boolean;
|
||||||
|
wasUpdated?: boolean;
|
||||||
|
error?: string
|
||||||
|
}> {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { email: true, hasMailbox: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
|
||||||
|
// Provider-Status prüfen
|
||||||
|
const providerStatus = await checkEmailExists(localPart);
|
||||||
|
|
||||||
|
if (!providerStatus.exists) {
|
||||||
|
return { success: true, hasMailbox: false, wasUpdated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerHasMailbox = providerStatus.hasMailbox === true;
|
||||||
|
|
||||||
|
// DB aktualisieren wenn Status abweicht
|
||||||
|
if (stressfreiEmail.hasMailbox !== providerHasMailbox) {
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { hasMailbox: providerHasMailbox },
|
||||||
|
});
|
||||||
|
console.log(`Mailbox-Status für ${stressfreiEmail.email} aktualisiert: ${stressfreiEmail.hasMailbox} -> ${providerHasMailbox}`);
|
||||||
|
return { success: true, hasMailbox: providerHasMailbox, wasUpdated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, hasMailbox: providerHasMailbox, wasUpdated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort für IMAP/SMTP-Zugang entschlüsseln (nur für autorisierte Nutzung)
|
||||||
|
export async function getDecryptedPassword(id: number): Promise<string | null> {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { emailPasswordEncrypted: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail?.emailPasswordEncrypted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
} catch {
|
||||||
|
console.error('Fehler beim Entschlüsseln des Passworts');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort neu generieren und beim Provider setzen
|
||||||
|
export async function resetMailboxPassword(id: number): Promise<{ success: boolean; password?: string; error?: string }> {
|
||||||
|
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { email: true, hasMailbox: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stressfreiEmail) {
|
||||||
|
return { success: false, error: 'StressfreiEmail nicht gefunden' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stressfreiEmail.hasMailbox) {
|
||||||
|
return { success: false, error: 'Keine Mailbox für diese E-Mail-Adresse' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neues Passwort generieren
|
||||||
|
const newPassword = generateSecurePassword();
|
||||||
|
const localPart = stressfreiEmail.email.split('@')[0];
|
||||||
|
|
||||||
|
// Passwort beim Provider ändern
|
||||||
|
const providerResult = await updateMailboxPassword(localPart, newPassword);
|
||||||
|
if (!providerResult.success) {
|
||||||
|
return { success: false, error: providerResult.error || 'Fehler beim Aktualisieren des Passworts beim Provider' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort verschlüsseln und lokal speichern
|
||||||
|
const passwordEncrypted = encrypt(newPassword);
|
||||||
|
|
||||||
|
await prisma.stressfreiEmail.update({
|
||||||
|
where: { id },
|
||||||
|
data: { emailPasswordEncrypted: passwordEncrypted },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, password: newPassword };
|
||||||
|
}
|
||||||
|
|||||||
@@ -134,10 +134,11 @@ export async function createUser(data: {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
roleIds: number[];
|
roleIds: number[];
|
||||||
customerId?: number;
|
customerId?: number;
|
||||||
|
hasDeveloperAccess?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||||
|
|
||||||
return prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
@@ -160,6 +161,13 @@ export async function createUser(data: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Entwicklerzugriff setzen falls aktiviert
|
||||||
|
if (data.hasDeveloperAccess) {
|
||||||
|
await setUserDeveloperAccess(user.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUser(
|
export async function updateUser(
|
||||||
@@ -270,10 +278,28 @@ export async function updateUser(
|
|||||||
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 10);
|
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user
|
// Prüfen ob Rollen geändert werden (für Zwangslogout)
|
||||||
|
let rolesChanged = false;
|
||||||
|
if (roleIds !== undefined) {
|
||||||
|
const currentRoles = await prisma.userRole.findMany({
|
||||||
|
where: { userId: id },
|
||||||
|
select: { roleId: true },
|
||||||
|
});
|
||||||
|
const currentRoleIds = currentRoles.map((r) => r.roleId).sort();
|
||||||
|
const newRoleIds = [...roleIds].sort();
|
||||||
|
rolesChanged =
|
||||||
|
currentRoleIds.length !== newRoleIds.length ||
|
||||||
|
!currentRoleIds.every((id, i) => id === newRoleIds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user - bei Rollenänderung Token invalidieren
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: userData,
|
data: {
|
||||||
|
...userData,
|
||||||
|
// Token invalidieren wenn Rollen geändert werden
|
||||||
|
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update roles if provided
|
// Update roles if provided
|
||||||
@@ -338,12 +364,22 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
|||||||
await prisma.userRole.create({
|
await prisma.userRole.create({
|
||||||
data: { userId, roleId: developerRole.id },
|
data: { userId, roleId: developerRole.id },
|
||||||
});
|
});
|
||||||
|
// Token invalidieren bei Rechteänderung
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { tokenInvalidatedAt: new Date() },
|
||||||
|
});
|
||||||
} else if (!enabled && hasRole) {
|
} else if (!enabled && hasRole) {
|
||||||
// Remove Developer role
|
// Remove Developer role
|
||||||
console.log('Removing Developer role');
|
console.log('Removing Developer role');
|
||||||
await prisma.userRole.delete({
|
await prisma.userRole.delete({
|
||||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||||
});
|
});
|
||||||
|
// Token invalidieren bei Rechteänderung
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: { tokenInvalidatedAt: new Date() },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface JwtPayload {
|
|||||||
customerId?: number; // Eigene Kunden-ID (bei Kundenportal-Login)
|
customerId?: number; // Eigene Kunden-ID (bei Kundenportal-Login)
|
||||||
isCustomerPortal?: boolean; // Ist dies ein Kundenportal-Login?
|
isCustomerPortal?: boolean; // Ist dies ein Kundenportal-Login?
|
||||||
representedCustomerIds?: number[]; // IDs der Kunden, die dieser Kunde vertreten kann
|
representedCustomerIds?: number[]; // IDs der Kunden, die dieser Kunde vertreten kann
|
||||||
|
iat?: number; // Token ausgestellt am (Unix Timestamp, automatisch von JWT)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthRequest extends Request {
|
export interface AuthRequest extends Request {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// ==================== PASSWORD GENERATOR ====================
|
||||||
|
// Generiert sichere, zufällige Passwörter
|
||||||
|
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
// Zeichensätze für Passwort-Generierung
|
||||||
|
const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const NUMBERS = '0123456789';
|
||||||
|
const SPECIAL = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||||
|
|
||||||
|
// Standard-Passwortlänge
|
||||||
|
const DEFAULT_LENGTH = 16;
|
||||||
|
|
||||||
|
export interface PasswordOptions {
|
||||||
|
length?: number;
|
||||||
|
includeLowercase?: boolean;
|
||||||
|
includeUppercase?: boolean;
|
||||||
|
includeNumbers?: boolean;
|
||||||
|
includeSpecial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert ein kryptografisch sicheres Passwort
|
||||||
|
*/
|
||||||
|
export function generateSecurePassword(options: PasswordOptions = {}): string {
|
||||||
|
const {
|
||||||
|
length = DEFAULT_LENGTH,
|
||||||
|
includeLowercase = true,
|
||||||
|
includeUppercase = true,
|
||||||
|
includeNumbers = true,
|
||||||
|
includeSpecial = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Zeichensatz zusammenstellen
|
||||||
|
let charset = '';
|
||||||
|
const requiredChars: string[] = [];
|
||||||
|
|
||||||
|
if (includeLowercase) {
|
||||||
|
charset += LOWERCASE;
|
||||||
|
requiredChars.push(getRandomChar(LOWERCASE));
|
||||||
|
}
|
||||||
|
if (includeUppercase) {
|
||||||
|
charset += UPPERCASE;
|
||||||
|
requiredChars.push(getRandomChar(UPPERCASE));
|
||||||
|
}
|
||||||
|
if (includeNumbers) {
|
||||||
|
charset += NUMBERS;
|
||||||
|
requiredChars.push(getRandomChar(NUMBERS));
|
||||||
|
}
|
||||||
|
if (includeSpecial) {
|
||||||
|
charset += SPECIAL;
|
||||||
|
requiredChars.push(getRandomChar(SPECIAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charset.length === 0) {
|
||||||
|
throw new Error('Mindestens ein Zeichensatz muss aktiviert sein');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restliche Zeichen auffüllen
|
||||||
|
const remainingLength = Math.max(0, length - requiredChars.length);
|
||||||
|
const randomChars: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < remainingLength; i++) {
|
||||||
|
randomChars.push(getRandomChar(charset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Zeichen mischen (Fisher-Yates Shuffle)
|
||||||
|
const allChars = [...requiredChars, ...randomChars];
|
||||||
|
for (let i = allChars.length - 1; i > 0; i--) {
|
||||||
|
const j = getRandomInt(i + 1);
|
||||||
|
[allChars[i], allChars[j]] = [allChars[j], allChars[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return allChars.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generiert ein einfaches Passwort ohne Sonderzeichen (für APIs die das nicht mögen)
|
||||||
|
*/
|
||||||
|
export function generateSimplePassword(length = 12): string {
|
||||||
|
return generateSecurePassword({
|
||||||
|
length,
|
||||||
|
includeLowercase: true,
|
||||||
|
includeUppercase: true,
|
||||||
|
includeNumbers: true,
|
||||||
|
includeSpecial: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kryptografisch sichere Zufallszahl
|
||||||
|
function getRandomInt(max: number): number {
|
||||||
|
const bytes = randomBytes(4);
|
||||||
|
const value = bytes.readUInt32BE(0);
|
||||||
|
return value % max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zufälliges Zeichen aus einem Zeichensatz
|
||||||
|
function getRandomChar(charset: string): string {
|
||||||
|
return charset[getRandomInt(charset.length)];
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
File diff suppressed because one or more lines are too long
+678
File diff suppressed because one or more lines are too long
-408
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OpenCRM</title>
|
<title>OpenCRM</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Cpkp9CHh.js"></script>
|
<script type="module" crossorigin src="/assets/index-CitfypIw.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-_pEZoPeF.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B7w5p8ZY.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+25
-2
@@ -791,8 +791,7 @@
|
|||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -1098,6 +1097,14 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/goober": {
|
||||||
|
"version": "2.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||||
|
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"csstype": "^3.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -1664,6 +1671,22 @@
|
|||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hot-toast": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.1.3",
|
||||||
|
"goober": "^2.1.16"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
|||||||
+19
-13
@@ -1,67 +1,73 @@
|
|||||||
{
|
{
|
||||||
"hash": "eeae1b35",
|
"hash": "b8db82d9",
|
||||||
"configHash": "c7be2068",
|
"configHash": "c7be2068",
|
||||||
"lockfileHash": "9edb0d4c",
|
"lockfileHash": "ee9bf28c",
|
||||||
"browserHash": "fe173fb8",
|
"browserHash": "deb47249",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"react": {
|
"react": {
|
||||||
"src": "../../react/index.js",
|
"src": "../../react/index.js",
|
||||||
"file": "react.js",
|
"file": "react.js",
|
||||||
"fileHash": "6771d2e5",
|
"fileHash": "304f33a9",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"src": "../../react-dom/index.js",
|
"src": "../../react-dom/index.js",
|
||||||
"file": "react-dom.js",
|
"file": "react-dom.js",
|
||||||
"fileHash": "4b5e071f",
|
"fileHash": "96d32bc1",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-dev-runtime": {
|
"react/jsx-dev-runtime": {
|
||||||
"src": "../../react/jsx-dev-runtime.js",
|
"src": "../../react/jsx-dev-runtime.js",
|
||||||
"file": "react_jsx-dev-runtime.js",
|
"file": "react_jsx-dev-runtime.js",
|
||||||
"fileHash": "053f8773",
|
"fileHash": "6424b7ea",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-runtime": {
|
"react/jsx-runtime": {
|
||||||
"src": "../../react/jsx-runtime.js",
|
"src": "../../react/jsx-runtime.js",
|
||||||
"file": "react_jsx-runtime.js",
|
"file": "react_jsx-runtime.js",
|
||||||
"fileHash": "5bc5ee39",
|
"fileHash": "af95a7a1",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"@tanstack/react-query": {
|
"@tanstack/react-query": {
|
||||||
"src": "../../@tanstack/react-query/build/modern/index.js",
|
"src": "../../@tanstack/react-query/build/modern/index.js",
|
||||||
"file": "@tanstack_react-query.js",
|
"file": "@tanstack_react-query.js",
|
||||||
"fileHash": "25a02139",
|
"fileHash": "c9459c14",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "7d1ba6fe",
|
"fileHash": "032e1913",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"lucide-react": {
|
"lucide-react": {
|
||||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||||
"file": "lucide-react.js",
|
"file": "lucide-react.js",
|
||||||
"fileHash": "3ebbc663",
|
"fileHash": "341675db",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-dom/client": {
|
"react-dom/client": {
|
||||||
"src": "../../react-dom/client.js",
|
"src": "../../react-dom/client.js",
|
||||||
"file": "react-dom_client.js",
|
"file": "react-dom_client.js",
|
||||||
"fileHash": "8b0b7734",
|
"fileHash": "6520a32f",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-hook-form": {
|
"react-hook-form": {
|
||||||
"src": "../../react-hook-form/dist/index.esm.mjs",
|
"src": "../../react-hook-form/dist/index.esm.mjs",
|
||||||
"file": "react-hook-form.js",
|
"file": "react-hook-form.js",
|
||||||
"fileHash": "d05e0352",
|
"fileHash": "29e58164",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router-dom": {
|
||||||
"src": "../../react-router-dom/dist/index.js",
|
"src": "../../react-router-dom/dist/index.js",
|
||||||
"file": "react-router-dom.js",
|
"file": "react-router-dom.js",
|
||||||
"fileHash": "0c95dc90",
|
"fileHash": "4b5d4fdc",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"react-hot-toast": {
|
||||||
|
"src": "../../react-hot-toast/dist/index.mjs",
|
||||||
|
"file": "react-hot-toast.js",
|
||||||
|
"fileHash": "7423c32d",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+486
@@ -0,0 +1,486 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
require_react
|
||||||
|
} from "./chunk-3TFVT2CW.js";
|
||||||
|
import {
|
||||||
|
__toESM
|
||||||
|
} from "./chunk-4MBMRILA.js";
|
||||||
|
|
||||||
|
// node_modules/react-hot-toast/dist/index.mjs
|
||||||
|
var import_react = __toESM(require_react(), 1);
|
||||||
|
var import_react2 = __toESM(require_react(), 1);
|
||||||
|
var y = __toESM(require_react(), 1);
|
||||||
|
|
||||||
|
// node_modules/goober/dist/goober.modern.js
|
||||||
|
var e = { data: "" };
|
||||||
|
var t = (t2) => {
|
||||||
|
if ("object" == typeof window) {
|
||||||
|
let e2 = (t2 ? t2.querySelector("#_goober") : window._goober) || Object.assign(document.createElement("style"), { innerHTML: " ", id: "_goober" });
|
||||||
|
return e2.nonce = window.__nonce__, e2.parentNode || (t2 || document.head).appendChild(e2), e2.firstChild;
|
||||||
|
}
|
||||||
|
return t2 || e;
|
||||||
|
};
|
||||||
|
var l = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
|
||||||
|
var a = /\/\*[^]*?\*\/| +/g;
|
||||||
|
var n = /\n+/g;
|
||||||
|
var o = (e2, t2) => {
|
||||||
|
let r = "", l2 = "", a2 = "";
|
||||||
|
for (let n3 in e2) {
|
||||||
|
let c2 = e2[n3];
|
||||||
|
"@" == n3[0] ? "i" == n3[1] ? r = n3 + " " + c2 + ";" : l2 += "f" == n3[1] ? o(c2, n3) : n3 + "{" + o(c2, "k" == n3[1] ? "" : t2) + "}" : "object" == typeof c2 ? l2 += o(c2, t2 ? t2.replace(/([^,])+/g, (e3) => n3.replace(/([^,]*:\S+\([^)]*\))|([^,])+/g, (t3) => /&/.test(t3) ? t3.replace(/&/g, e3) : e3 ? e3 + " " + t3 : t3)) : n3) : null != c2 && (n3 = /^--/.test(n3) ? n3 : n3.replace(/[A-Z]/g, "-$&").toLowerCase(), a2 += o.p ? o.p(n3, c2) : n3 + ":" + c2 + ";");
|
||||||
|
}
|
||||||
|
return r + (t2 && a2 ? t2 + "{" + a2 + "}" : a2) + l2;
|
||||||
|
};
|
||||||
|
var c = {};
|
||||||
|
var s = (e2) => {
|
||||||
|
if ("object" == typeof e2) {
|
||||||
|
let t2 = "";
|
||||||
|
for (let r in e2) t2 += r + s(e2[r]);
|
||||||
|
return t2;
|
||||||
|
}
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
var i = (e2, t2, r, i2, p2) => {
|
||||||
|
let u2 = s(e2), d2 = c[u2] || (c[u2] = ((e3) => {
|
||||||
|
let t3 = 0, r2 = 11;
|
||||||
|
for (; t3 < e3.length; ) r2 = 101 * r2 + e3.charCodeAt(t3++) >>> 0;
|
||||||
|
return "go" + r2;
|
||||||
|
})(u2));
|
||||||
|
if (!c[d2]) {
|
||||||
|
let t3 = u2 !== e2 ? e2 : ((e3) => {
|
||||||
|
let t4, r2, o2 = [{}];
|
||||||
|
for (; t4 = l.exec(e3.replace(a, "")); ) t4[4] ? o2.shift() : t4[3] ? (r2 = t4[3].replace(n, " ").trim(), o2.unshift(o2[0][r2] = o2[0][r2] || {})) : o2[0][t4[1]] = t4[2].replace(n, " ").trim();
|
||||||
|
return o2[0];
|
||||||
|
})(e2);
|
||||||
|
c[d2] = o(p2 ? { ["@keyframes " + d2]: t3 } : t3, r ? "" : "." + d2);
|
||||||
|
}
|
||||||
|
let f3 = r && c.g ? c.g : null;
|
||||||
|
return r && (c.g = c[d2]), ((e3, t3, r2, l2) => {
|
||||||
|
l2 ? t3.data = t3.data.replace(l2, e3) : -1 === t3.data.indexOf(e3) && (t3.data = r2 ? e3 + t3.data : t3.data + e3);
|
||||||
|
})(c[d2], t2, i2, f3), d2;
|
||||||
|
};
|
||||||
|
var p = (e2, t2, r) => e2.reduce((e3, l2, a2) => {
|
||||||
|
let n3 = t2[a2];
|
||||||
|
if (n3 && n3.call) {
|
||||||
|
let e4 = n3(r), t3 = e4 && e4.props && e4.props.className || /^go/.test(e4) && e4;
|
||||||
|
n3 = t3 ? "." + t3 : e4 && "object" == typeof e4 ? e4.props ? "" : o(e4, "") : false === e4 ? "" : e4;
|
||||||
|
}
|
||||||
|
return e3 + l2 + (null == n3 ? "" : n3);
|
||||||
|
}, "");
|
||||||
|
function u(e2) {
|
||||||
|
let r = this || {}, l2 = e2.call ? e2(r.p) : e2;
|
||||||
|
return i(l2.unshift ? l2.raw ? p(l2, [].slice.call(arguments, 1), r.p) : l2.reduce((e3, t2) => Object.assign(e3, t2 && t2.call ? t2(r.p) : t2), {}) : l2, t(r.target), r.g, r.o, r.k);
|
||||||
|
}
|
||||||
|
var d;
|
||||||
|
var f;
|
||||||
|
var g;
|
||||||
|
var b = u.bind({ g: 1 });
|
||||||
|
var h = u.bind({ k: 1 });
|
||||||
|
function m(e2, t2, r, l2) {
|
||||||
|
o.p = t2, d = e2, f = r, g = l2;
|
||||||
|
}
|
||||||
|
function w(e2, t2) {
|
||||||
|
let r = this || {};
|
||||||
|
return function() {
|
||||||
|
let l2 = arguments;
|
||||||
|
function a2(n3, o2) {
|
||||||
|
let c2 = Object.assign({}, n3), s2 = c2.className || a2.className;
|
||||||
|
r.p = Object.assign({ theme: f && f() }, c2), r.o = / *go\d+/.test(s2), c2.className = u.apply(r, l2) + (s2 ? " " + s2 : ""), t2 && (c2.ref = o2);
|
||||||
|
let i2 = e2;
|
||||||
|
return e2[0] && (i2 = c2.as || e2, delete c2.as), g && i2[0] && g(c2), d(i2, c2);
|
||||||
|
}
|
||||||
|
return t2 ? t2(a2) : a2;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// node_modules/react-hot-toast/dist/index.mjs
|
||||||
|
var b2 = __toESM(require_react(), 1);
|
||||||
|
var x = __toESM(require_react(), 1);
|
||||||
|
var Z = (e2) => typeof e2 == "function";
|
||||||
|
var h2 = (e2, t2) => Z(e2) ? e2(t2) : e2;
|
||||||
|
var W = /* @__PURE__ */ (() => {
|
||||||
|
let e2 = 0;
|
||||||
|
return () => (++e2).toString();
|
||||||
|
})();
|
||||||
|
var E = /* @__PURE__ */ (() => {
|
||||||
|
let e2;
|
||||||
|
return () => {
|
||||||
|
if (e2 === void 0 && typeof window < "u") {
|
||||||
|
let t2 = matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
|
e2 = !t2 || t2.matches;
|
||||||
|
}
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
var re = 20;
|
||||||
|
var k = "default";
|
||||||
|
var H = (e2, t2) => {
|
||||||
|
let { toastLimit: o2 } = e2.settings;
|
||||||
|
switch (t2.type) {
|
||||||
|
case 0:
|
||||||
|
return { ...e2, toasts: [t2.toast, ...e2.toasts].slice(0, o2) };
|
||||||
|
case 1:
|
||||||
|
return { ...e2, toasts: e2.toasts.map((r) => r.id === t2.toast.id ? { ...r, ...t2.toast } : r) };
|
||||||
|
case 2:
|
||||||
|
let { toast: s2 } = t2;
|
||||||
|
return H(e2, { type: e2.toasts.find((r) => r.id === s2.id) ? 1 : 0, toast: s2 });
|
||||||
|
case 3:
|
||||||
|
let { toastId: a2 } = t2;
|
||||||
|
return { ...e2, toasts: e2.toasts.map((r) => r.id === a2 || a2 === void 0 ? { ...r, dismissed: true, visible: false } : r) };
|
||||||
|
case 4:
|
||||||
|
return t2.toastId === void 0 ? { ...e2, toasts: [] } : { ...e2, toasts: e2.toasts.filter((r) => r.id !== t2.toastId) };
|
||||||
|
case 5:
|
||||||
|
return { ...e2, pausedAt: t2.time };
|
||||||
|
case 6:
|
||||||
|
let i2 = t2.time - (e2.pausedAt || 0);
|
||||||
|
return { ...e2, pausedAt: void 0, toasts: e2.toasts.map((r) => ({ ...r, pauseDuration: r.pauseDuration + i2 })) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var v = [];
|
||||||
|
var j = { toasts: [], pausedAt: void 0, settings: { toastLimit: re } };
|
||||||
|
var f2 = {};
|
||||||
|
var Y = (e2, t2 = k) => {
|
||||||
|
f2[t2] = H(f2[t2] || j, e2), v.forEach(([o2, s2]) => {
|
||||||
|
o2 === t2 && s2(f2[t2]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var _ = (e2) => Object.keys(f2).forEach((t2) => Y(e2, t2));
|
||||||
|
var Q = (e2) => Object.keys(f2).find((t2) => f2[t2].toasts.some((o2) => o2.id === e2));
|
||||||
|
var S = (e2 = k) => (t2) => {
|
||||||
|
Y(t2, e2);
|
||||||
|
};
|
||||||
|
var se = { blank: 4e3, error: 4e3, success: 2e3, loading: 1 / 0, custom: 4e3 };
|
||||||
|
var V = (e2 = {}, t2 = k) => {
|
||||||
|
let [o2, s2] = (0, import_react.useState)(f2[t2] || j), a2 = (0, import_react.useRef)(f2[t2]);
|
||||||
|
(0, import_react.useEffect)(() => (a2.current !== f2[t2] && s2(f2[t2]), v.push([t2, s2]), () => {
|
||||||
|
let r = v.findIndex(([l2]) => l2 === t2);
|
||||||
|
r > -1 && v.splice(r, 1);
|
||||||
|
}), [t2]);
|
||||||
|
let i2 = o2.toasts.map((r) => {
|
||||||
|
var l2, g2, T;
|
||||||
|
return { ...e2, ...e2[r.type], ...r, removeDelay: r.removeDelay || ((l2 = e2[r.type]) == null ? void 0 : l2.removeDelay) || (e2 == null ? void 0 : e2.removeDelay), duration: r.duration || ((g2 = e2[r.type]) == null ? void 0 : g2.duration) || (e2 == null ? void 0 : e2.duration) || se[r.type], style: { ...e2.style, ...(T = e2[r.type]) == null ? void 0 : T.style, ...r.style } };
|
||||||
|
});
|
||||||
|
return { ...o2, toasts: i2 };
|
||||||
|
};
|
||||||
|
var ie = (e2, t2 = "blank", o2) => ({ createdAt: Date.now(), visible: true, dismissed: false, type: t2, ariaProps: { role: "status", "aria-live": "polite" }, message: e2, pauseDuration: 0, ...o2, id: (o2 == null ? void 0 : o2.id) || W() });
|
||||||
|
var P = (e2) => (t2, o2) => {
|
||||||
|
let s2 = ie(t2, e2, o2);
|
||||||
|
return S(s2.toasterId || Q(s2.id))({ type: 2, toast: s2 }), s2.id;
|
||||||
|
};
|
||||||
|
var n2 = (e2, t2) => P("blank")(e2, t2);
|
||||||
|
n2.error = P("error");
|
||||||
|
n2.success = P("success");
|
||||||
|
n2.loading = P("loading");
|
||||||
|
n2.custom = P("custom");
|
||||||
|
n2.dismiss = (e2, t2) => {
|
||||||
|
let o2 = { type: 3, toastId: e2 };
|
||||||
|
t2 ? S(t2)(o2) : _(o2);
|
||||||
|
};
|
||||||
|
n2.dismissAll = (e2) => n2.dismiss(void 0, e2);
|
||||||
|
n2.remove = (e2, t2) => {
|
||||||
|
let o2 = { type: 4, toastId: e2 };
|
||||||
|
t2 ? S(t2)(o2) : _(o2);
|
||||||
|
};
|
||||||
|
n2.removeAll = (e2) => n2.remove(void 0, e2);
|
||||||
|
n2.promise = (e2, t2, o2) => {
|
||||||
|
let s2 = n2.loading(t2.loading, { ...o2, ...o2 == null ? void 0 : o2.loading });
|
||||||
|
return typeof e2 == "function" && (e2 = e2()), e2.then((a2) => {
|
||||||
|
let i2 = t2.success ? h2(t2.success, a2) : void 0;
|
||||||
|
return i2 ? n2.success(i2, { id: s2, ...o2, ...o2 == null ? void 0 : o2.success }) : n2.dismiss(s2), a2;
|
||||||
|
}).catch((a2) => {
|
||||||
|
let i2 = t2.error ? h2(t2.error, a2) : void 0;
|
||||||
|
i2 ? n2.error(i2, { id: s2, ...o2, ...o2 == null ? void 0 : o2.error }) : n2.dismiss(s2);
|
||||||
|
}), e2;
|
||||||
|
};
|
||||||
|
var ce = 1e3;
|
||||||
|
var w2 = (e2, t2 = "default") => {
|
||||||
|
let { toasts: o2, pausedAt: s2 } = V(e2, t2), a2 = (0, import_react2.useRef)(/* @__PURE__ */ new Map()).current, i2 = (0, import_react2.useCallback)((c2, m2 = ce) => {
|
||||||
|
if (a2.has(c2)) return;
|
||||||
|
let p2 = setTimeout(() => {
|
||||||
|
a2.delete(c2), r({ type: 4, toastId: c2 });
|
||||||
|
}, m2);
|
||||||
|
a2.set(c2, p2);
|
||||||
|
}, []);
|
||||||
|
(0, import_react2.useEffect)(() => {
|
||||||
|
if (s2) return;
|
||||||
|
let c2 = Date.now(), m2 = o2.map((p2) => {
|
||||||
|
if (p2.duration === 1 / 0) return;
|
||||||
|
let R = (p2.duration || 0) + p2.pauseDuration - (c2 - p2.createdAt);
|
||||||
|
if (R < 0) {
|
||||||
|
p2.visible && n2.dismiss(p2.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return setTimeout(() => n2.dismiss(p2.id, t2), R);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
m2.forEach((p2) => p2 && clearTimeout(p2));
|
||||||
|
};
|
||||||
|
}, [o2, s2, t2]);
|
||||||
|
let r = (0, import_react2.useCallback)(S(t2), [t2]), l2 = (0, import_react2.useCallback)(() => {
|
||||||
|
r({ type: 5, time: Date.now() });
|
||||||
|
}, [r]), g2 = (0, import_react2.useCallback)((c2, m2) => {
|
||||||
|
r({ type: 1, toast: { id: c2, height: m2 } });
|
||||||
|
}, [r]), T = (0, import_react2.useCallback)(() => {
|
||||||
|
s2 && r({ type: 6, time: Date.now() });
|
||||||
|
}, [s2, r]), d2 = (0, import_react2.useCallback)((c2, m2) => {
|
||||||
|
let { reverseOrder: p2 = false, gutter: R = 8, defaultPosition: z } = m2 || {}, O = o2.filter((u2) => (u2.position || z) === (c2.position || z) && u2.height), K = O.findIndex((u2) => u2.id === c2.id), B = O.filter((u2, I) => I < K && u2.visible).length;
|
||||||
|
return O.filter((u2) => u2.visible).slice(...p2 ? [B + 1] : [0, B]).reduce((u2, I) => u2 + (I.height || 0) + R, 0);
|
||||||
|
}, [o2]);
|
||||||
|
return (0, import_react2.useEffect)(() => {
|
||||||
|
o2.forEach((c2) => {
|
||||||
|
if (c2.dismissed) i2(c2.id, c2.removeDelay);
|
||||||
|
else {
|
||||||
|
let m2 = a2.get(c2.id);
|
||||||
|
m2 && (clearTimeout(m2), a2.delete(c2.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [o2, i2]), { toasts: o2, handlers: { updateHeight: g2, startPause: l2, endPause: T, calculateOffset: d2 } };
|
||||||
|
};
|
||||||
|
var de = h`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(45deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(45deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
var me = h`
|
||||||
|
from {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
var le = h`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(90deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(90deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
var C = w("div")`
|
||||||
|
width: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: ${(e2) => e2.primary || "#ff4b4b"};
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
|
||||||
|
animation: ${de} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
animation-delay: 100ms;
|
||||||
|
|
||||||
|
&:after,
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
animation: ${me} 0.15s ease-out forwards;
|
||||||
|
animation-delay: 150ms;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
background: ${(e2) => e2.secondary || "#fff"};
|
||||||
|
bottom: 9px;
|
||||||
|
left: 4px;
|
||||||
|
height: 2px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
animation: ${le} 0.15s ease-out forwards;
|
||||||
|
animation-delay: 180ms;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
var Te = h`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
var F = w("div")`
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 100%;
|
||||||
|
border-color: ${(e2) => e2.secondary || "#e0e0e0"};
|
||||||
|
border-right-color: ${(e2) => e2.primary || "#616161"};
|
||||||
|
animation: ${Te} 1s linear infinite;
|
||||||
|
`;
|
||||||
|
var ge = h`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(45deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(45deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
var he = h`
|
||||||
|
0% {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
height: 0;
|
||||||
|
width: 6px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
height: 10px;
|
||||||
|
}`;
|
||||||
|
var L = w("div")`
|
||||||
|
width: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: ${(e2) => e2.primary || "#61d345"};
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
|
||||||
|
animation: ${ge} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
animation-delay: 100ms;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: ${he} 0.2s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
position: absolute;
|
||||||
|
border-right: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-color: ${(e2) => e2.secondary || "#fff"};
|
||||||
|
bottom: 6px;
|
||||||
|
left: 6px;
|
||||||
|
height: 10px;
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
var be = w("div")`
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
var Se = w("div")`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
`;
|
||||||
|
var Ae = h`
|
||||||
|
from {
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
var Pe = w("div")`
|
||||||
|
position: relative;
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0.4;
|
||||||
|
min-width: 20px;
|
||||||
|
animation: ${Ae} 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
`;
|
||||||
|
var $ = ({ toast: e2 }) => {
|
||||||
|
let { icon: t2, type: o2, iconTheme: s2 } = e2;
|
||||||
|
return t2 !== void 0 ? typeof t2 == "string" ? b2.createElement(Pe, null, t2) : t2 : o2 === "blank" ? null : b2.createElement(Se, null, b2.createElement(F, { ...s2 }), o2 !== "loading" && b2.createElement(be, null, o2 === "error" ? b2.createElement(C, { ...s2 }) : b2.createElement(L, { ...s2 })));
|
||||||
|
};
|
||||||
|
var Re = (e2) => `
|
||||||
|
0% {transform: translate3d(0,${e2 * -200}%,0) scale(.6); opacity:.5;}
|
||||||
|
100% {transform: translate3d(0,0,0) scale(1); opacity:1;}
|
||||||
|
`;
|
||||||
|
var Ee = (e2) => `
|
||||||
|
0% {transform: translate3d(0,0,-1px) scale(1); opacity:1;}
|
||||||
|
100% {transform: translate3d(0,${e2 * -150}%,-1px) scale(.6); opacity:0;}
|
||||||
|
`;
|
||||||
|
var ve = "0%{opacity:0;} 100%{opacity:1;}";
|
||||||
|
var De = "0%{opacity:1;} 100%{opacity:0;}";
|
||||||
|
var Oe = w("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
color: #363636;
|
||||||
|
line-height: 1.3;
|
||||||
|
will-change: transform;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
max-width: 350px;
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
`;
|
||||||
|
var Ie = w("div")`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px 10px;
|
||||||
|
color: inherit;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
white-space: pre-line;
|
||||||
|
`;
|
||||||
|
var ke = (e2, t2) => {
|
||||||
|
let s2 = e2.includes("top") ? 1 : -1, [a2, i2] = E() ? [ve, De] : [Re(s2), Ee(s2)];
|
||||||
|
return { animation: t2 ? `${h(a2)} 0.35s cubic-bezier(.21,1.02,.73,1) forwards` : `${h(i2)} 0.4s forwards cubic-bezier(.06,.71,.55,1)` };
|
||||||
|
};
|
||||||
|
var N = y.memo(({ toast: e2, position: t2, style: o2, children: s2 }) => {
|
||||||
|
let a2 = e2.height ? ke(e2.position || t2 || "top-center", e2.visible) : { opacity: 0 }, i2 = y.createElement($, { toast: e2 }), r = y.createElement(Ie, { ...e2.ariaProps }, h2(e2.message, e2));
|
||||||
|
return y.createElement(Oe, { className: e2.className, style: { ...a2, ...o2, ...e2.style } }, typeof s2 == "function" ? s2({ icon: i2, message: r }) : y.createElement(y.Fragment, null, i2, r));
|
||||||
|
});
|
||||||
|
m(x.createElement);
|
||||||
|
var we = ({ id: e2, className: t2, style: o2, onHeightUpdate: s2, children: a2 }) => {
|
||||||
|
let i2 = x.useCallback((r) => {
|
||||||
|
if (r) {
|
||||||
|
let l2 = () => {
|
||||||
|
let g2 = r.getBoundingClientRect().height;
|
||||||
|
s2(e2, g2);
|
||||||
|
};
|
||||||
|
l2(), new MutationObserver(l2).observe(r, { subtree: true, childList: true, characterData: true });
|
||||||
|
}
|
||||||
|
}, [e2, s2]);
|
||||||
|
return x.createElement("div", { ref: i2, className: t2, style: o2 }, a2);
|
||||||
|
};
|
||||||
|
var Me = (e2, t2) => {
|
||||||
|
let o2 = e2.includes("top"), s2 = o2 ? { top: 0 } : { bottom: 0 }, a2 = e2.includes("center") ? { justifyContent: "center" } : e2.includes("right") ? { justifyContent: "flex-end" } : {};
|
||||||
|
return { left: 0, right: 0, display: "flex", position: "absolute", transition: E() ? void 0 : "all 230ms cubic-bezier(.21,1.02,.73,1)", transform: `translateY(${t2 * (o2 ? 1 : -1)}px)`, ...s2, ...a2 };
|
||||||
|
};
|
||||||
|
var Ce = u`
|
||||||
|
z-index: 9999;
|
||||||
|
> * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
var D = 16;
|
||||||
|
var Fe = ({ reverseOrder: e2, position: t2 = "top-center", toastOptions: o2, gutter: s2, children: a2, toasterId: i2, containerStyle: r, containerClassName: l2 }) => {
|
||||||
|
let { toasts: g2, handlers: T } = w2(o2, i2);
|
||||||
|
return x.createElement("div", { "data-rht-toaster": i2 || "", style: { position: "fixed", zIndex: 9999, top: D, left: D, right: D, bottom: D, pointerEvents: "none", ...r }, className: l2, onMouseEnter: T.startPause, onMouseLeave: T.endPause }, g2.map((d2) => {
|
||||||
|
let c2 = d2.position || t2, m2 = T.calculateOffset(d2, { reverseOrder: e2, gutter: s2, defaultPosition: t2 }), p2 = Me(c2, m2);
|
||||||
|
return x.createElement(we, { id: d2.id, key: d2.id, onHeightUpdate: T.updateHeight, className: d2.visible ? Ce : "", style: p2 }, d2.type === "custom" ? h2(d2.message, d2) : a2 ? a2(d2) : x.createElement(N, { toast: d2, position: c2 }));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
var zt = n2;
|
||||||
|
export {
|
||||||
|
L as CheckmarkIcon,
|
||||||
|
C as ErrorIcon,
|
||||||
|
F as LoaderIcon,
|
||||||
|
N as ToastBar,
|
||||||
|
$ as ToastIcon,
|
||||||
|
Fe as Toaster,
|
||||||
|
zt as default,
|
||||||
|
h2 as resolveValue,
|
||||||
|
n2 as toast,
|
||||||
|
w2 as useToaster,
|
||||||
|
V as useToasterStore
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=react-hot-toast.js.map
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user