added backup and email client
This commit is contained in:
parent
ff857be01a
commit
e4fdfbc95f
258
README.md
258
README.md
|
|
@ -292,76 +292,216 @@ fetch('/api/developer/setup', { method: 'POST' }).then(r => r.json()).then(conso
|
|||
# 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
|
||||
- **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
|
||||
> **Hinweis:** Vertragstypen können nur von Benutzern mit **Entwicklerzugriff** geändert werden, da Änderungen auch Anpassungen an den Formularen erfordern.
|
||||
|
||||
#### Technische Umsetzung
|
||||
### Vertragstyp-spezifische Felder
|
||||
|
||||
1. **IMAP/SMTP-Zugangsdaten speichern**
|
||||
- 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)
|
||||
Je nach Vertragstyp werden unterschiedliche Felder im Formular angezeigt:
|
||||
|
||||
2. **Backend: IMAP-Service**
|
||||
- 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
|
||||
#### Strom & Gas (ELECTRICITY, GAS)
|
||||
|
||||
3. **Frontend: E-Mail-Tab (Kundenansicht)**
|
||||
- Neuer Tab in CustomerDetail: "E-Mails"
|
||||
- Dropdown oben: Auswahl der StressfreiEmail-Adresse
|
||||
- Liste der E-Mails (Betreff, Absender, Datum)
|
||||
- Detail-Ansicht beim Klick
|
||||
- "Neue E-Mail" und "Antworten" Buttons
|
||||
- "Vertrag zuordnen" Dropdown in E-Mail-Detail
|
||||
- Zähler-Auswahl
|
||||
- Jahresverbrauch (kWh/m³)
|
||||
- Grundpreis, Arbeitspreis
|
||||
- Bonus
|
||||
- Vorversorger, Kundennummer beim Vorversorger
|
||||
|
||||
4. **Frontend: E-Mail-Tab (Vertragsansicht)**
|
||||
- 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
|
||||
#### Internet (DSL, CABLE, FIBER)
|
||||
|
||||
5. **Datenbank**
|
||||
```prisma
|
||||
model CustomerEmail {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
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())
|
||||
}
|
||||
```
|
||||
- Download/Upload (Mbit/s)
|
||||
- Router Modell, Seriennummer
|
||||
- Installationsdatum
|
||||
- Benutzername, Passwort
|
||||
- Rufnummern mit SIP-Zugangsdaten
|
||||
|
||||
#### Status
|
||||
- [ ] Noch nicht implementiert - Plan für zukünftige Version
|
||||
| Vertragstyp | Zusatzfeld |
|
||||
|-------------|------------|
|
||||
| **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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 updateEmail(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
|
||||
|
|
@ -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.updateEmail = updateEmail;
|
||||
exports.deleteEmail = deleteEmail;
|
||||
exports.resetPassword = resetPassword;
|
||||
const stressfreiEmailService = __importStar(require("../services/stressfreiEmail.service.js"));
|
||||
async function getEmailsByCustomer(req, res) {
|
||||
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
|
||||
|
|
@ -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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"}
|
||||
|
|
@ -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 appSetting_routes_js_1 = __importDefault(require("./routes/appSetting.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();
|
||||
const app = (0, express_1.default)();
|
||||
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/settings', appSetting_routes_js_1.default);
|
||||
app.use('/api/email-providers', emailProvider_routes_js_1.default);
|
||||
app.use('/api', cachedEmail_routes_js_1.default);
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
|
|
|
|||
|
|
@ -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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"}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Response, NextFunction } from 'express';
|
||||
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 requireCustomerAccess(req: AuthRequest, res: Response, next: NextFunction): void;
|
||||
//# sourceMappingURL=auth.d.ts.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -7,15 +7,48 @@ exports.authenticate = authenticate;
|
|||
exports.requirePermission = requirePermission;
|
||||
exports.requireCustomerAccess = requireCustomerAccess;
|
||||
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;
|
||||
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' });
|
||||
return;
|
||||
}
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
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;
|
||||
next();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
{"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"}
|
||||
|
|
@ -32,10 +32,28 @@ var __importStar = (this && this.__importStar) || (function () {
|
|||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const multer_1 = __importDefault(require("multer"));
|
||||
const appSettingController = __importStar(require("../controllers/appSetting.controller.js"));
|
||||
const backupController = __importStar(require("../controllers/backup.controller.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)();
|
||||
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
||||
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);
|
||||
// Mehrere Einstellungen aktualisieren (nur Admin)
|
||||
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;
|
||||
//# sourceMappingURL=appSetting.routes.js.map
|
||||
|
|
@ -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 @@
|
|||
{"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"}
|
||||
|
|
@ -37,10 +37,12 @@ const express_1 = require("express");
|
|||
const contractCategoryController = __importStar(require("../controllers/contractCategory.controller.js"));
|
||||
const auth_js_1 = require("../middleware/auth.js");
|
||||
const router = (0, express_1.Router)();
|
||||
// Lesen für alle authentifizierten Benutzer
|
||||
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.put('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('platforms:update'), contractCategoryController.updateContractCategory);
|
||||
router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('platforms:delete'), contractCategoryController.deleteContractCategory);
|
||||
// Ändern/Löschen nur mit Entwickler-Berechtigung (Vertragstypen erfordern Formular-Anpassungen)
|
||||
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;
|
||||
//# sourceMappingURL=contractCategory.routes.js.map
|
||||
|
|
@ -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 @@
|
|||
{"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.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);
|
||||
// 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;
|
||||
//# sourceMappingURL=stressfreiEmail.routes.js.map
|
||||
|
|
@ -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"}
|
||||
|
|
@ -247,10 +247,12 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
} | null;
|
||||
carInsuranceDetails: {
|
||||
id: number;
|
||||
|
|
@ -511,6 +513,11 @@ export declare function getContractById(id: number, decryptPassword?: boolean):
|
|||
portalUsername: string | null;
|
||||
stressfreiEmailId: number | null;
|
||||
}) | null;
|
||||
followUpContract: {
|
||||
id: number;
|
||||
status: import(".prisma/client").$Enums.ContractStatus;
|
||||
contractNumber: string;
|
||||
} | null;
|
||||
} & {
|
||||
id: number;
|
||||
customerId: number;
|
||||
|
|
@ -936,10 +943,12 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
} | null;
|
||||
carInsuranceDetails: {
|
||||
id: number;
|
||||
|
|
@ -1200,6 +1209,11 @@ export declare function updateContract(id: number, data: Partial<ContractCreateD
|
|||
portalUsername: string | null;
|
||||
stressfreiEmailId: number | null;
|
||||
}) | null;
|
||||
followUpContract: {
|
||||
id: number;
|
||||
status: import(".prisma/client").$Enums.ContractStatus;
|
||||
contractNumber: string;
|
||||
} | null;
|
||||
} & {
|
||||
id: number;
|
||||
customerId: number;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -27,8 +27,14 @@ async function getAllContracts(filters) {
|
|||
}
|
||||
if (type)
|
||||
where.type = type;
|
||||
if (status)
|
||||
// Status-Filter: Deaktivierte Verträge standardmäßig ausblenden
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
else {
|
||||
// Wenn kein Status-Filter gesetzt, alle außer DEACTIVATED anzeigen
|
||||
where.status = { not: client_1.ContractStatus.DEACTIVATED };
|
||||
}
|
||||
if (search) {
|
||||
where.OR = [
|
||||
// Basis-Vertragsfelder
|
||||
|
|
@ -125,6 +131,9 @@ async function getContractById(id, decryptPassword = false) {
|
|||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
stressfreiEmail: true,
|
||||
followUpContract: {
|
||||
select: { id: true, contractNumber: true, status: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!contract)
|
||||
|
|
@ -412,6 +421,19 @@ async function updateContract(id, data) {
|
|||
return getContractById(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 } });
|
||||
}
|
||||
async function createFollowUpContract(previousContractId) {
|
||||
|
|
@ -419,6 +441,14 @@ async function createFollowUpContract(previousContractId) {
|
|||
if (!previousContract) {
|
||||
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
|
||||
const newContractData = {
|
||||
customerId: previousContract.customerId,
|
||||
|
|
@ -441,6 +471,7 @@ async function createFollowUpContract(previousContractId) {
|
|||
annualConsumption: previousContract.energyDetails.annualConsumption ?? undefined,
|
||||
basePrice: previousContract.energyDetails.basePrice ?? undefined,
|
||||
unitPrice: previousContract.energyDetails.unitPrice ?? undefined,
|
||||
bonus: previousContract.energyDetails.bonus ?? undefined,
|
||||
previousProviderName: previousContract.providerName ?? undefined,
|
||||
previousCustomerNumber: previousContract.customerNumberAtProvider ?? undefined,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -128,10 +128,12 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
}[];
|
||||
contracts: ({
|
||||
address: {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -108,6 +108,10 @@ async function getCustomerById(id) {
|
|||
},
|
||||
stressfreiEmails: { orderBy: { isActive: 'desc' } },
|
||||
contracts: {
|
||||
where: {
|
||||
// Deaktivierte Verträge ausblenden
|
||||
status: { not: client_1.ContractStatus.DEACTIVATED },
|
||||
},
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
include: {
|
||||
address: true,
|
||||
|
|
|
|||
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<{
|
||||
id: number;
|
||||
isActive: boolean;
|
||||
|
|
@ -13,6 +13,13 @@ export declare function getAllProviderConfigs(): Promise<{
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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<{
|
||||
id: number;
|
||||
|
|
@ -28,6 +35,13 @@ export declare function getProviderConfigById(id: number): Promise<{
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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>;
|
||||
export declare function getDefaultProviderConfig(): Promise<{
|
||||
id: number;
|
||||
|
|
@ -43,6 +57,13 @@ export declare function getDefaultProviderConfig(): Promise<{
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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>;
|
||||
export declare function getActiveProviderConfig(): Promise<{
|
||||
id: number;
|
||||
|
|
@ -58,6 +79,13 @@ export declare function getActiveProviderConfig(): Promise<{
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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>;
|
||||
export interface CreateProviderConfigData {
|
||||
name: string;
|
||||
|
|
@ -68,6 +96,9 @@ export interface CreateProviderConfigData {
|
|||
password?: string;
|
||||
domain: string;
|
||||
defaultForwardEmail?: string;
|
||||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -85,6 +116,13 @@ export declare function createProviderConfig(data: CreateProviderConfigData): Pr
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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<{
|
||||
id: number;
|
||||
|
|
@ -100,6 +138,13 @@ export declare function updateProviderConfig(id: number, data: Partial<CreatePro
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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<{
|
||||
id: number;
|
||||
|
|
@ -115,9 +160,32 @@ export declare function deleteProviderConfig(id: number): Promise<{
|
|||
passwordEncrypted: string | null;
|
||||
domain: string;
|
||||
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 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 renameProvisionedEmail(oldLocalPart: string, newLocalPart: string): Promise<EmailOperationResult>;
|
||||
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.checkEmailExists = checkEmailExists;
|
||||
exports.provisionEmail = provisionEmail;
|
||||
exports.provisionEmailWithMailbox = provisionEmailWithMailbox;
|
||||
exports.enableMailboxForExistingEmail = enableMailboxForExistingEmail;
|
||||
exports.updateMailboxPassword = updateMailboxPassword;
|
||||
exports.getImapSmtpSettings = getImapSmtpSettings;
|
||||
exports.deprovisionEmail = deprovisionEmail;
|
||||
exports.renameProvisionedEmail = renameProvisionedEmail;
|
||||
exports.getProviderDomain = getProviderDomain;
|
||||
|
|
@ -79,6 +83,9 @@ async function createProviderConfig(data) {
|
|||
passwordEncrypted,
|
||||
domain: data.domain,
|
||||
defaultForwardEmail: data.defaultForwardEmail || null,
|
||||
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
|
|
@ -107,6 +114,12 @@ async function updateProviderConfig(id, data) {
|
|||
updateData.domain = data.domain;
|
||||
if (data.defaultForwardEmail !== undefined)
|
||||
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.isDefault !== undefined)
|
||||
|
|
@ -159,6 +172,13 @@ async function getProviderInstance() {
|
|||
password,
|
||||
domain: dbConfig.domain,
|
||||
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,
|
||||
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
|
||||
async function deprovisionEmail(localPart) {
|
||||
try {
|
||||
|
|
@ -284,6 +434,13 @@ async function getProviderInstanceById(id) {
|
|||
password,
|
||||
domain: dbConfig.domain,
|
||||
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,
|
||||
isDefault: dbConfig.isDefault,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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 {
|
||||
readonly type = "PLESK";
|
||||
private config;
|
||||
|
|
@ -8,6 +8,9 @@ export declare class PleskEmailProvider implements IEmailProvider {
|
|||
testConnection(): Promise<void>;
|
||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||
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>;
|
||||
renameEmail(params: RenameEmailParams): 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"}
|
||||
|
|
@ -137,9 +137,18 @@ class PleskEmailProvider {
|
|||
}
|
||||
// stdout sollte die Mail-Infos enthalten
|
||||
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 {
|
||||
exists,
|
||||
email: exists ? email : undefined,
|
||||
hasMailbox,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -169,11 +178,12 @@ class PleskEmailProvider {
|
|||
}
|
||||
// Plesk CLI API: Mail-Account mit Weiterleitung erstellen
|
||||
// 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', {
|
||||
params: [
|
||||
'--create', email,
|
||||
'-forwarding', 'true',
|
||||
'-forwarding-addresses', forwardTargets.join(','),
|
||||
'-forwarding-addresses', `add:${forwardTargets.join(',')}`,
|
||||
'-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) {
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
try {
|
||||
|
|
@ -271,11 +393,12 @@ class PleskEmailProvider {
|
|||
};
|
||||
}
|
||||
// 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', {
|
||||
params: [
|
||||
'--update', email,
|
||||
'-forwarding', 'true',
|
||||
'-forwarding-addresses', targets.join(','),
|
||||
'-forwarding-addresses', `set:${targets.join(',')}`,
|
||||
],
|
||||
});
|
||||
return {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,4 @@
|
|||
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||
export interface EmailForwardTarget {
|
||||
email: string;
|
||||
}
|
||||
|
|
@ -5,6 +6,22 @@ export interface CreateEmailParams {
|
|||
localPart: 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 {
|
||||
oldLocalPart: string;
|
||||
newLocalPart: string;
|
||||
|
|
@ -12,6 +29,7 @@ export interface RenameEmailParams {
|
|||
export interface EmailExistsResult {
|
||||
exists: boolean;
|
||||
email?: string;
|
||||
hasMailbox?: boolean;
|
||||
}
|
||||
export interface EmailOperationResult {
|
||||
success: boolean;
|
||||
|
|
@ -23,6 +41,9 @@ export interface IEmailProvider {
|
|||
testConnection(): Promise<void>;
|
||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||
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>;
|
||||
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
||||
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
||||
|
|
@ -37,6 +58,13 @@ export interface EmailProviderConfig {
|
|||
password?: string;
|
||||
domain: string;
|
||||
defaultForwardEmail?: string;
|
||||
imapServer?: string;
|
||||
imapPort?: number;
|
||||
smtpServer?: string;
|
||||
smtpPort?: number;
|
||||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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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"}
|
||||
|
|
@ -6,10 +6,21 @@ export declare function getEmailsByCustomerId(customerId: number, includeInactiv
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | 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<{
|
||||
id: number;
|
||||
|
|
@ -19,17 +30,14 @@ export declare function getEmailById(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
} | null>;
|
||||
export declare function createEmail(data: {
|
||||
customerId: number;
|
||||
email: string;
|
||||
platform?: string;
|
||||
notes?: string;
|
||||
}): Promise<{
|
||||
export declare function getEmailWithMailboxById(id: number): Promise<{
|
||||
id: number;
|
||||
email: string;
|
||||
customerId: number;
|
||||
|
|
@ -37,10 +45,32 @@ export declare function createEmail(data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
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;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
}>;
|
||||
export declare function updateEmail(id: number, data: {
|
||||
email?: string;
|
||||
|
|
@ -55,10 +85,12 @@ export declare function updateEmail(id: number, data: {
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | null;
|
||||
provisionError: string | null;
|
||||
emailPasswordEncrypted: string | null;
|
||||
}>;
|
||||
export declare function deleteEmail(id: number): Promise<{
|
||||
id: number;
|
||||
|
|
@ -68,9 +100,27 @@ export declare function deleteEmail(id: number): Promise<{
|
|||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
notes: string | null;
|
||||
hasMailbox: boolean;
|
||||
platform: string | null;
|
||||
isProvisioned: boolean;
|
||||
provisionedAt: Date | 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
|
||||
|
|
@ -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"}
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getEmailsByCustomerId = getEmailsByCustomerId;
|
||||
exports.getEmailsWithMailboxByCustomerId = getEmailsWithMailboxByCustomerId;
|
||||
exports.getEmailById = getEmailById;
|
||||
exports.getEmailWithMailboxById = getEmailWithMailboxById;
|
||||
exports.createEmail = createEmail;
|
||||
exports.updateEmail = updateEmail;
|
||||
exports.deleteEmail = deleteEmail;
|
||||
exports.enableMailbox = enableMailbox;
|
||||
exports.syncMailboxStatus = syncMailboxStatus;
|
||||
exports.getDecryptedPassword = getDecryptedPassword;
|
||||
exports.resetMailboxPassword = resetMailboxPassword;
|
||||
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();
|
||||
async function getEmailsByCustomerId(customerId, includeInactive = false) {
|
||||
const where = { customerId };
|
||||
|
|
@ -17,16 +26,96 @@ async function getEmailsByCustomerId(customerId, includeInactive = false) {
|
|||
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) {
|
||||
return prisma.stressfreiEmail.findUnique({
|
||||
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) {
|
||||
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({
|
||||
data: {
|
||||
...data,
|
||||
...emailData,
|
||||
isActive: true,
|
||||
hasMailbox: createMailbox || false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -39,4 +128,105 @@ async function updateEmail(id, data) {
|
|||
async function deleteEmail(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
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -71,6 +71,7 @@ export declare function createUser(data: {
|
|||
lastName: string;
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
}): Promise<{
|
||||
id: number;
|
||||
email: string;
|
||||
|
|
@ -137,6 +138,7 @@ export declare function deleteUser(id: number): Promise<{
|
|||
firstName: string;
|
||||
lastName: string;
|
||||
isActive: boolean;
|
||||
tokenInvalidatedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -122,7 +122,7 @@ async function getUserById(id) {
|
|||
}
|
||||
async function createUser(data) {
|
||||
const hashedPassword = await bcryptjs_1.default.hash(data.password, 10);
|
||||
return prisma.user.create({
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
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) {
|
||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||
|
|
@ -224,10 +229,27 @@ async function updateUser(id, data) {
|
|||
if (password) {
|
||||
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({
|
||||
where: { id },
|
||||
data: userData,
|
||||
data: {
|
||||
...userData,
|
||||
// Token invalidieren wenn Rollen geändert werden
|
||||
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
|
||||
},
|
||||
});
|
||||
// Update roles if provided
|
||||
if (roleIds) {
|
||||
|
|
@ -281,6 +303,11 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||
await prisma.userRole.create({
|
||||
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) {
|
||||
// Remove Developer role
|
||||
|
|
@ -288,6 +315,11 @@ async function setUserDeveloperAccess(userId, enabled) {
|
|||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
// Token invalidieren bei Rechteänderung
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,7 @@ export interface JwtPayload {
|
|||
customerId?: number;
|
||||
isCustomerPortal?: boolean;
|
||||
representedCustomerIds?: number[];
|
||||
iat?: number;
|
||||
}
|
||||
export interface AuthRequest extends Request {
|
||||
user?: JwtPayload;
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -137,6 +137,7 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
isActive: 'isActive',
|
||||
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||
customerId: 'customerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
|
|
@ -259,6 +260,13 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||
passwordEncrypted: 'passwordEncrypted',
|
||||
domain: 'domain',
|
||||
defaultForwardEmail: 'defaultForwardEmail',
|
||||
imapServer: 'imapServer',
|
||||
imapPort: 'imapPort',
|
||||
smtpServer: 'smtpServer',
|
||||
smtpPort: 'smtpPort',
|
||||
imapEncryption: 'imapEncryption',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||
isActive: 'isActive',
|
||||
isDefault: 'isDefault',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -275,6 +283,36 @@ exports.Prisma.StressfreiEmailScalarFieldEnum = {
|
|||
isProvisioned: 'isProvisioned',
|
||||
provisionedAt: 'provisionedAt',
|
||||
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',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
|
@ -542,6 +580,17 @@ exports.EmailProviderType = exports.$Enums.EmailProviderType = {
|
|||
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 = {
|
||||
ELECTRICITY: 'ELECTRICITY',
|
||||
GAS: 'GAS'
|
||||
|
|
@ -592,6 +641,7 @@ exports.Prisma.ModelName = {
|
|||
IdentityDocument: 'IdentityDocument',
|
||||
EmailProviderConfig: 'EmailProviderConfig',
|
||||
StressfreiEmail: 'StressfreiEmail',
|
||||
CachedEmail: 'CachedEmail',
|
||||
Meter: 'Meter',
|
||||
MeterReading: 'MeterReading',
|
||||
SalesPlatform: 'SalesPlatform',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "prisma-client-f3be941c86c0d933a2a09d69aafc49ad121411869df4ce4f365fdf53679b90db",
|
||||
"name": "prisma-client-3c4bb688688ba372393d0bf86523c07e8b4de3ff0d9ad23a89f905f15047a1a5",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
|
|
|||
|
|
@ -20,17 +20,18 @@ model AppSetting {
|
|||
// ==================== USERS & AUTH ====================
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Role {
|
||||
|
|
@ -216,6 +217,13 @@ enum EmailProviderType {
|
|||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // z.B. "Plesk Hauptserver"
|
||||
|
|
@ -226,28 +234,103 @@ model EmailProviderConfig {
|
|||
passwordEncrypted String? // Passwort (verschlüsselt)
|
||||
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
||||
defaultForwardEmail String? // Standard-Weiterleitungsadresse (unsere eigene)
|
||||
isActive Boolean @default(true)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// 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)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
||||
|
||||
model StressfreiEmail {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
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
|
||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
|
||||
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
|
||||
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) ====================
|
||||
|
|
@ -464,7 +547,8 @@ model Contract {
|
|||
tvDetails TvContractDetails?
|
||||
carInsuranceDetails CarInsuranceDetails?
|
||||
|
||||
tasks ContractTask[]
|
||||
tasks ContractTask[]
|
||||
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
isActive: 'isActive',
|
||||
tokenInvalidatedAt: 'tokenInvalidatedAt',
|
||||
customerId: 'customerId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
|
|
@ -259,6 +260,13 @@ exports.Prisma.EmailProviderConfigScalarFieldEnum = {
|
|||
passwordEncrypted: 'passwordEncrypted',
|
||||
domain: 'domain',
|
||||
defaultForwardEmail: 'defaultForwardEmail',
|
||||
imapServer: 'imapServer',
|
||||
imapPort: 'imapPort',
|
||||
smtpServer: 'smtpServer',
|
||||
smtpPort: 'smtpPort',
|
||||
imapEncryption: 'imapEncryption',
|
||||
smtpEncryption: 'smtpEncryption',
|
||||
allowSelfSignedCerts: 'allowSelfSignedCerts',
|
||||
isActive: 'isActive',
|
||||
isDefault: 'isDefault',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -275,6 +283,36 @@ exports.Prisma.StressfreiEmailScalarFieldEnum = {
|
|||
isProvisioned: 'isProvisioned',
|
||||
provisionedAt: 'provisionedAt',
|
||||
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',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
|
@ -542,6 +580,17 @@ exports.EmailProviderType = exports.$Enums.EmailProviderType = {
|
|||
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 = {
|
||||
ELECTRICITY: 'ELECTRICITY',
|
||||
GAS: 'GAS'
|
||||
|
|
@ -592,6 +641,7 @@ exports.Prisma.ModelName = {
|
|||
IdentityDocument: 'IdentityDocument',
|
||||
EmailProviderConfig: 'EmailProviderConfig',
|
||||
StressfreiEmail: 'StressfreiEmail',
|
||||
CachedEmail: 'CachedEmail',
|
||||
Meter: 'Meter',
|
||||
MeterReading: 'MeterReading',
|
||||
SalesPlatform: 'SalesPlatform',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,26 +13,37 @@
|
|||
"db:migrate": "prisma migrate dev",
|
||||
"db:push": "prisma db push",
|
||||
"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": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"express-validator": "^7.2.0",
|
||||
"imapflow": "^1.2.8",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailparser": "^3.9.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^7.0.13",
|
||||
"undici": "^6.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/nodemailer": "^7.0.9",
|
||||
"prisma": "^5.22.0",
|
||||
"tsx": "^4.19.2",
|
||||
"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();
|
||||
});
|
||||
|
|
@ -20,17 +20,18 @@ model AppSetting {
|
|||
// ==================== USERS & AUTH ====================
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
firstName String
|
||||
lastName String
|
||||
isActive Boolean @default(true)
|
||||
tokenInvalidatedAt DateTime? // Zeitpunkt ab dem alle Tokens ungültig sind (für Zwangslogout bei Rechteänderung)
|
||||
customerId Int? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
roles UserRole[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Role {
|
||||
|
|
@ -216,6 +217,13 @@ enum EmailProviderType {
|
|||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // z.B. "Plesk Hauptserver"
|
||||
|
|
@ -226,6 +234,18 @@ model EmailProviderConfig {
|
|||
passwordEncrypted String? // Passwort (verschlüsselt)
|
||||
domain String // Domain für E-Mails (z.B. stressfrei-wechseln.de)
|
||||
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)
|
||||
isDefault Boolean @default(false) // Standard-Provider
|
||||
createdAt DateTime @default(now())
|
||||
|
|
@ -235,19 +255,82 @@ model EmailProviderConfig {
|
|||
// ==================== STRESSFREI-WECHSELN EMAIL ADDRESSES ====================
|
||||
|
||||
model StressfreiEmail {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
contracts Contract[] // Verträge die diese E-Mail als Benutzername verwenden
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
email String // Die Weiterleitungs-E-Mail-Adresse
|
||||
platform String? // Für welche Plattform (z.B. "Freenet", "Klarmobil")
|
||||
notes String? @db.Text // Optionale Notizen
|
||||
isActive Boolean @default(true)
|
||||
isProvisioned Boolean @default(false) // Wurde bei Provider angelegt?
|
||||
provisionedAt DateTime? // Wann wurde provisioniert?
|
||||
provisionError String? @db.Text // Fehlermeldung falls Provisionierung fehlschlug
|
||||
|
||||
// Mailbox-Zugangsdaten (für IMAP/SMTP-Zugang)
|
||||
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) ====================
|
||||
|
|
@ -465,6 +548,7 @@ model Contract {
|
|||
carInsuranceDetails CarInsuranceDetails?
|
||||
|
||||
tasks ContractTask[]
|
||||
assignedEmails CachedEmail[] // Zugeordnete E-Mails aus dem E-Mail-Client
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
|
|
|||
|
|
@ -6,17 +6,31 @@ const prisma = new PrismaClient();
|
|||
async function main() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
// Create permissions
|
||||
const resources = ['customers', 'contracts', 'users', 'platforms', 'providers', 'developer'];
|
||||
const actions = ['create', 'read', 'update', 'delete', 'access'];
|
||||
// ==================== PERMISSIONS ====================
|
||||
// Ressourcen mit ihren erlaubten Aktionen
|
||||
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 }[] = [];
|
||||
for (const resource of resources) {
|
||||
for (const [resource, actions] of Object.entries(resourcePermissions)) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +43,7 @@ async function main() {
|
|||
});
|
||||
}
|
||||
|
||||
console.log('Permissions created');
|
||||
console.log(`Permissions created (${permissions.length} total)`);
|
||||
|
||||
// Get all permissions
|
||||
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
|
||||
.filter(
|
||||
(p) =>
|
||||
p.resource === 'customers' ||
|
||||
p.resource === 'contracts' ||
|
||||
(p.resource === 'platforms' && p.action === 'read') ||
|
||||
(p.resource === 'providers' && p.action === 'read')
|
||||
// Read-only Zugriff auf Stammdaten und Konfiguration
|
||||
(p.action === 'read' && [
|
||||
'platforms',
|
||||
'providers',
|
||||
'tariffs',
|
||||
'cancellation-periods',
|
||||
'contract-durations',
|
||||
'contract-categories',
|
||||
].includes(p.resource))
|
||||
)
|
||||
.map((p) => p.id);
|
||||
|
||||
|
|
@ -86,10 +120,20 @@ async function main() {
|
|||
},
|
||||
});
|
||||
|
||||
// Read-only employee
|
||||
const readOnlyPermIds = [customerReadPerm?.id, contractReadPerm?.id, platformReadPerm?.id, providerReadPerm?.id].filter(
|
||||
(id): id is number => id !== undefined
|
||||
);
|
||||
// Read-only employee - read access to main entities and lookup tables
|
||||
const readOnlyResources = [
|
||||
'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({
|
||||
where: { name: 'Mitarbeiter (Nur-Lesen)' },
|
||||
|
|
@ -149,15 +193,76 @@ async function main() {
|
|||
|
||||
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)
|
||||
const contractCategories = [
|
||||
{ code: 'ELECTRICITY', name: 'Strom', icon: 'Zap', color: '#FFC107', sortOrder: 1 },
|
||||
{ code: 'GAS', name: 'Gas', icon: 'Flame', color: '#FF5722', sortOrder: 2 },
|
||||
{ code: 'DSL', name: 'DSL', icon: 'Wifi', color: '#2196F3', sortOrder: 3 },
|
||||
{ code: 'FIBER', name: 'Glasfaser', icon: 'Cable', color: '#9C27B0', sortOrder: 4 },
|
||||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 5 },
|
||||
{ code: 'TV', name: 'TV', icon: 'Tv', color: '#E91E63', sortOrder: 6 },
|
||||
{ code: 'CAR_INSURANCE', name: 'KFZ-Versicherung', icon: 'Car', color: '#607D8B', sortOrder: 7 },
|
||||
{ code: 'CABLE', name: 'Kabel Internet (Coax)', icon: 'Cable', color: '#00BCD4', sortOrder: 5 },
|
||||
{ code: 'MOBILE', name: 'Mobilfunk', icon: 'Smartphone', color: '#4CAF50', sortOrder: 6 },
|
||||
{ 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) {
|
||||
|
|
@ -170,6 +275,77 @@ async function main() {
|
|||
|
||||
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!');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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 appSettingRoutes from './routes/appSetting.routes.js';
|
||||
import emailProviderRoutes from './routes/emailProvider.routes.js';
|
||||
import cachedEmailRoutes from './routes/cachedEmail.routes.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ app.use('/api/contract-categories', contractCategoryRoutes);
|
|||
app.use('/api', contractTaskRoutes);
|
||||
app.use('/api/settings', appSettingRoutes);
|
||||
app.use('/api/email-providers', emailProviderRoutes);
|
||||
app.use('/api', cachedEmailRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,64 @@
|
|||
import { Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest, JwtPayload } from '../types/index.js';
|
||||
|
||||
export function authenticate(
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function authenticate(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
): Promise<void> {
|
||||
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' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
process.env.JWT_SECRET || 'fallback-secret'
|
||||
) 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;
|
||||
next();
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import * as appSettingController from '../controllers/appSetting.controller.js';
|
||||
import * as backupController from '../controllers/backup.controller.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();
|
||||
|
||||
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
||||
|
|
@ -26,4 +41,63 @@ router.put(
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
// Lesen für alle authentifizierten Benutzer
|
||||
router.get('/', authenticate, contractCategoryController.getContractCategories);
|
||||
router.post('/', authenticate, requirePermission('platforms:create'), contractCategoryController.createContractCategory);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -9,4 +9,7 @@ router.get('/:id', authenticate, requirePermission('customers:read'), stressfrei
|
|||
router.put('/:id', authenticate, requirePermission('customers:update'), stressfreiEmailController.updateEmail);
|
||||
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;
|
||||
|
|
|
|||
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,
|
||||
EmailOperationResult,
|
||||
CreateEmailParams,
|
||||
MailEncryption,
|
||||
} from './types.js';
|
||||
import { PleskEmailProvider } from './pleskProvider.js';
|
||||
|
||||
|
|
@ -68,6 +69,10 @@ export interface CreateProviderConfigData {
|
|||
password?: string;
|
||||
domain: string;
|
||||
defaultForwardEmail?: string;
|
||||
// Verschlüsselungs-Einstellungen
|
||||
imapEncryption?: MailEncryption;
|
||||
smtpEncryption?: MailEncryption;
|
||||
allowSelfSignedCerts?: boolean;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -95,6 +100,9 @@ export async function createProviderConfig(data: CreateProviderConfigData) {
|
|||
passwordEncrypted,
|
||||
domain: data.domain,
|
||||
defaultForwardEmail: data.defaultForwardEmail || null,
|
||||
imapEncryption: data.imapEncryption ?? 'SSL',
|
||||
smtpEncryption: data.smtpEncryption ?? 'SSL',
|
||||
allowSelfSignedCerts: data.allowSelfSignedCerts ?? false,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
|
|
@ -123,6 +131,9 @@ export async function updateProviderConfig(
|
|||
if (data.domain !== undefined) updateData.domain = data.domain;
|
||||
if (data.defaultForwardEmail !== undefined)
|
||||
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.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
||||
|
||||
|
|
@ -179,6 +190,13 @@ async function getProviderInstance(): Promise<IEmailProvider> {
|
|||
password,
|
||||
domain: dbConfig.domain,
|
||||
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,
|
||||
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
|
||||
export async function deprovisionEmail(localPart: string): Promise<EmailOperationResult> {
|
||||
try {
|
||||
|
|
@ -328,6 +509,13 @@ async function getProviderInstanceById(id: number): Promise<IEmailProvider> {
|
|||
password,
|
||||
domain: dbConfig.domain,
|
||||
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,
|
||||
isDefault: dbConfig.isDefault,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import {
|
|||
EmailExistsResult,
|
||||
EmailOperationResult,
|
||||
CreateEmailParams,
|
||||
CreateEmailWithMailboxParams,
|
||||
CreateEmailWithMailboxResult,
|
||||
EnableMailboxParams,
|
||||
UpdateMailboxPasswordParams,
|
||||
RenameEmailParams,
|
||||
} from './types.js';
|
||||
|
||||
|
|
@ -173,9 +177,20 @@ export class PleskEmailProvider implements IEmailProvider {
|
|||
|
||||
// stdout sollte die Mail-Infos enthalten
|
||||
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 {
|
||||
exists,
|
||||
email: exists ? email : undefined,
|
||||
hasMailbox,
|
||||
};
|
||||
} catch (error) {
|
||||
// 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> {
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
// ==================== EMAIL PROVIDER TYPES ====================
|
||||
|
||||
// Verschlüsselungstyp für E-Mail-Verbindungen
|
||||
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
||||
|
||||
export interface EmailForwardTarget {
|
||||
email: string;
|
||||
}
|
||||
|
|
@ -9,6 +12,27 @@ export interface CreateEmailParams {
|
|||
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 {
|
||||
oldLocalPart: string;
|
||||
newLocalPart: string;
|
||||
|
|
@ -17,6 +41,7 @@ export interface RenameEmailParams {
|
|||
export interface EmailExistsResult {
|
||||
exists: boolean;
|
||||
email?: string;
|
||||
hasMailbox?: boolean; // true wenn echte Mailbox vorhanden
|
||||
}
|
||||
|
||||
export interface EmailOperationResult {
|
||||
|
|
@ -36,9 +61,18 @@ export interface IEmailProvider {
|
|||
// Prüft ob eine E-Mail-Adresse existiert
|
||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||
|
||||
// Erstellt eine neue E-Mail-Weiterleitung
|
||||
// Erstellt eine neue E-Mail-Weiterleitung (ohne Mailbox)
|
||||
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
|
||||
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
||||
|
||||
|
|
@ -60,6 +94,15 @@ export interface EmailProviderConfig {
|
|||
password?: string; // Entschlüsselt
|
||||
domain: 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;
|
||||
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 { 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();
|
||||
|
||||
|
|
@ -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) {
|
||||
return prisma.stressfreiEmail.findUnique({
|
||||
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;
|
||||
email: string;
|
||||
platform?: 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({
|
||||
data: {
|
||||
...data,
|
||||
...emailData,
|
||||
isActive: true,
|
||||
hasMailbox: createMailbox || false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -51,3 +155,136 @@ export async function updateEmail(
|
|||
export async function deleteEmail(id: number) {
|
||||
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;
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
return prisma.user.create({
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
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(
|
||||
|
|
@ -270,10 +278,28 @@ export async function updateUser(
|
|||
(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({
|
||||
where: { id },
|
||||
data: userData,
|
||||
data: {
|
||||
...userData,
|
||||
// Token invalidieren wenn Rollen geändert werden
|
||||
...(rolesChanged && { tokenInvalidatedAt: new Date() }),
|
||||
},
|
||||
});
|
||||
|
||||
// Update roles if provided
|
||||
|
|
@ -338,12 +364,22 @@ async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
|||
await prisma.userRole.create({
|
||||
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) {
|
||||
// Remove Developer role
|
||||
console.log('Removing Developer role');
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
// Token invalidieren bei Rechteänderung
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { tokenInvalidatedAt: new Date() },
|
||||
});
|
||||
} else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface JwtPayload {
|
|||
customerId?: number; // Eigene Kunden-ID (bei Kundenportal-Login)
|
||||
isCustomerPortal?: boolean; // Ist dies ein Kundenportal-Login?
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenCRM</title>
|
||||
<script type="module" crossorigin src="/assets/index-Cpkp9CHh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-_pEZoPeF.css">
|
||||
<script type="module" crossorigin src="/assets/index-CitfypIw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B7w5p8ZY.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -791,8 +791,7 @@
|
|||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
|
|
@ -1098,6 +1097,14 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -1664,6 +1671,22 @@
|
|||
"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": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,67 +1,73 @@
|
|||
{
|
||||
"hash": "eeae1b35",
|
||||
"hash": "b8db82d9",
|
||||
"configHash": "c7be2068",
|
||||
"lockfileHash": "9edb0d4c",
|
||||
"browserHash": "fe173fb8",
|
||||
"lockfileHash": "ee9bf28c",
|
||||
"browserHash": "deb47249",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "6771d2e5",
|
||||
"fileHash": "304f33a9",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "4b5e071f",
|
||||
"fileHash": "96d32bc1",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "053f8773",
|
||||
"fileHash": "6424b7ea",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "5bc5ee39",
|
||||
"fileHash": "af95a7a1",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@tanstack/react-query": {
|
||||
"src": "../../@tanstack/react-query/build/modern/index.js",
|
||||
"file": "@tanstack_react-query.js",
|
||||
"fileHash": "25a02139",
|
||||
"fileHash": "c9459c14",
|
||||
"needsInterop": false
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "7d1ba6fe",
|
||||
"fileHash": "032e1913",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "3ebbc663",
|
||||
"fileHash": "341675db",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "8b0b7734",
|
||||
"fileHash": "6520a32f",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-hook-form": {
|
||||
"src": "../../react-hook-form/dist/index.esm.mjs",
|
||||
"file": "react-hook-form.js",
|
||||
"fileHash": "d05e0352",
|
||||
"fileHash": "29e58164",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Cristian Bote
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,857 @@
|
|||
<p align="center">
|
||||
<img src="./goober_cover.png" width="500" alt="goober" />
|
||||
</p>
|
||||
|
||||
🥜 goober, a less than 1KB css-in-js solution.
|
||||
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
|
||||
[](https://www.npmjs.com/package/goober)
|
||||
[](https://travis-ci.org/cristianbote/goober)
|
||||
[](https://unpkg.com/goober)
|
||||
[](https://www.npmjs.com/package/goober)
|
||||
[](https://codecov.io/github/cristianbote/goober?branch=master)
|
||||
[](https://join.slack.com/t/gooberdev/shared_invite/enQtOTM5NjUyOTcwNzI1LWUwNzg0NTQwODY1NDJmMzQ2NzdlODI4YTM3NWUwYjlkY2ZkNGVmMTFlNGMwZGUyOWQyZmI4OTYwYmRiMzE0NGQ)
|
||||
|
||||
# 🪒 The Great Shave Off Challenge
|
||||
|
||||
Can you shave off bytes from goober? Do it and you're gonna get paid! [More info here](https://goober.rocks/the-great-shave-off)
|
||||
|
||||
# Motivation
|
||||
|
||||
I've always wondered if you could get a working solution for css-in-js with a smaller footprint. While I was working on a side project I wanted to use styled-components, or more accurately the `styled` pattern. Looking at the JavaScript bundle sizes, I quickly realized that I would have to include ~12kB([styled-components](https://github.com/styled-components/styled-components)) or ~11kB([emotion](https://github.com/emotion-js/emotion)) just so I can use the `styled` paradigm. So, I embarked on a mission to create a smaller alternative for these well established APIs.
|
||||
|
||||
# Why the peanuts emoji?
|
||||
|
||||
It's a pun on the tagline.
|
||||
|
||||
> css-in-js at the cost of peanuts!
|
||||
> 🥜goober
|
||||
|
||||
# Talks and Podcasts
|
||||
|
||||
* [React Round Up](https://reactroundup.com/wrangle-your-css-in-js-for-peanuts-using-goober-ft-cristian-bote-rru-177) 👉 https://reactroundup.com/wrangle-your-css-in-js-for-peanuts-using-goober-ft-cristian-bote-rru-177
|
||||
* ReactDay Berlin 2019 👉 https://www.youtube.com/watch?v=k4-AVy3acqk
|
||||
* [PodRocket](https://podrocket.logrocket.com/) by [LogRocket](https://logrocket.com/) 👉 https://podrocket.logrocket.com/goober
|
||||
* [ngParty](https://www.ngparty.cz/) 👉 https://www.youtube.com/watch?v=XKFvOBDPeB0
|
||||
|
||||
# Table of contents
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Examples](#examples)
|
||||
- [Tradeoffs](#comparison-and-tradeoffs)
|
||||
- [SSR](#ssr)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Browser](#browser)
|
||||
- [SSR](#ssr-1)
|
||||
- [API](#api)
|
||||
- [styled](#styledtagname-string--function-forwardref-function)
|
||||
- [setup](#setuppragma-function-prefixer-function-theme-function-forwardprops-function)
|
||||
- [With prefixer](#with-prefixer)
|
||||
- [With theme](#with-theme)
|
||||
- [With forwardProps](#with-forwardProps)
|
||||
- [css](#csstaggedtemplate)
|
||||
- [targets](#targets)
|
||||
- [extractCss](#extractcsstarget)
|
||||
- [createGlobalStyles](#createglobalstyles)
|
||||
- [keyframes](#keyframes)
|
||||
- [shouldForwardProp](#shouldForwardProp)
|
||||
- [Integrations](#integrations)
|
||||
- [Babel Plugin](#babel-plugin)
|
||||
- [Babel Macro Plugin](#babel-macro-plugin)
|
||||
- [Next.js](#nextjs)
|
||||
- [Gatsby](#gatsby)
|
||||
- [Preact CLI Plugin](#preact-cli-plugin)
|
||||
- [CSS Prop](#css-prop)
|
||||
- [Features](#features)
|
||||
- [Sharing Style](#sharing-style)
|
||||
- [Autoprefixer](#autoprefixer)
|
||||
- [TypeScript](#typescript)
|
||||
- [Content Security Policy (CSP)](#content-security-policy-csp)-
|
||||
- [Browser Support](#browser-support)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
# Usage
|
||||
|
||||
The API is inspired by emotion `styled` function. Meaning, you call it with your `tagName`, and it returns a vDOM component for that tag. Note, `setup` needs to be ran before the `styled` function is used.
|
||||
|
||||
```jsx
|
||||
import { h } from 'preact';
|
||||
import { styled, setup } from 'goober';
|
||||
|
||||
// Should be called here, and just once
|
||||
setup(h);
|
||||
|
||||
const Icon = styled('span')`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
color: red;
|
||||
`;
|
||||
|
||||
const Button = styled('button')`
|
||||
background: dodgerblue;
|
||||
color: white;
|
||||
border: ${Math.random()}px solid white;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.otherClass {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
${Icon} {
|
||||
color: black;
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
- [Vanilla](https://codesandbox.io/s/qlywyp7z4q)
|
||||
- [React](https://codesandbox.io/s/k0mnp40n7v)
|
||||
- [Preact](https://codesandbox.io/s/r15wj2qm7o)
|
||||
- [SSR with Preact](https://codesandbox.io/s/7m9zzl6746)
|
||||
- [Fre](https://codesandbox.io/s/fre-goober-ffqjv)
|
||||
|
||||
# Comparison and tradeoffs
|
||||
|
||||
In this section I would like to compare goober, as objectively as I can, with the latest versions of two most well known css-in-js packages: styled-components and emotion.
|
||||
|
||||
I've used the following markers to reflect the state of each feature:
|
||||
|
||||
- ✅ Supported
|
||||
- 🟡 Partially supported
|
||||
- 🛑 Not supported
|
||||
|
||||
Here we go:
|
||||
|
||||
| Feature name | Goober | Styled Components | Emotion |
|
||||
| ---------------------- | ------- | ----------------- | ------- |
|
||||
| Base bundle size | 1.25 kB | 12.6 kB | 7.4 kB |
|
||||
| Framework agnostic | ✅ | 🛑 | 🛑 |
|
||||
| Render with target \*1 | ✅ | 🛑 | 🛑 |
|
||||
| `css` api | ✅ | ✅ | ✅ |
|
||||
| `css` prop | ✅ | ✅ | ✅ |
|
||||
| `styled` | ✅ | ✅ | ✅ |
|
||||
| `styled.<tag>` | ✅ \*2 | ✅ | ✅ |
|
||||
| default export | 🛑 | ✅ | ✅ |
|
||||
| `as` | ✅ | ✅ | ✅ |
|
||||
| `.withComponent` | 🛑 | ✅ | ✅ |
|
||||
| `.attrs` | 🛑 | ✅ | 🛑 |
|
||||
| `shouldForwardProp` | ✅ | ✅ | ✅ |
|
||||
| `keyframes` | ✅ | ✅ | ✅ |
|
||||
| Labels | 🛑 | 🛑 | ✅ |
|
||||
| ClassNames | 🛑 | 🛑 | ✅ |
|
||||
| Global styles | ✅ | ✅ | ✅ |
|
||||
| SSR | ✅ | ✅ | ✅ |
|
||||
| Theming | ✅ | ✅ | ✅ |
|
||||
| Tagged Templates | ✅ | ✅ | ✅ |
|
||||
| Object styles | ✅ | ✅ | ✅ |
|
||||
| Dynamic styles | ✅ | ✅ | ✅ |
|
||||
|
||||
Footnotes
|
||||
|
||||
- [1] `goober` can render in _any_ dom target. Meaning you can use `goober` to define scoped styles in any context. Really useful for web-components.
|
||||
- [2] Supported only via `babel-plugin-transform-goober`
|
||||
|
||||
# SSR
|
||||
|
||||
You can get the critical CSS for SSR via `extractCss`. Take a look at this example: [CodeSandbox: SSR with Preact and goober](https://codesandbox.io/s/7m9zzl6746) and read the full explanation for `extractCSS` and `targets` below.
|
||||
|
||||
# Benchmarks
|
||||
|
||||
The results are included inside the build output as well.
|
||||
|
||||
## Browser
|
||||
|
||||
Coming soon!
|
||||
|
||||
## SSR
|
||||
|
||||
The benchmark is testing the following scenario:
|
||||
|
||||
```jsx
|
||||
import styled from '<packageName>';
|
||||
|
||||
// Create the dynamic styled component
|
||||
const Foo = styled('div')((props) => ({
|
||||
opacity: props.counter > 0.5 ? 1 : 0,
|
||||
'@media (min-width: 1px)': {
|
||||
rule: 'all'
|
||||
},
|
||||
'&:hover': {
|
||||
another: 1,
|
||||
display: 'space'
|
||||
}
|
||||
}));
|
||||
|
||||
// Serialize the component
|
||||
renderToString(<Foo counter={Math.random()} />);
|
||||
```
|
||||
|
||||
The results are:
|
||||
|
||||
```
|
||||
goober x 200,437 ops/sec ±1.93% (87 runs sampled)
|
||||
styled-components@5.2.1 x 12,650 ops/sec ±9.09% (48 runs sampled)
|
||||
emotion@11.0.0 x 104,229 ops/sec ±2.06% (88 runs sampled)
|
||||
|
||||
Fastest is: goober
|
||||
```
|
||||
|
||||
# API
|
||||
|
||||
As you can see, goober supports most of the CSS syntax. If you find any issues, please submit a ticket, or open a PR with a fix.
|
||||
|
||||
### `styled(tagName: String | Function, forwardRef?: Function)`
|
||||
|
||||
- `@param {String|Function} tagName` The name of the DOM element you'd like the styles to be applied to
|
||||
- `@param {Function} forwardRef` Forward ref function. Usually `React.forwardRef`
|
||||
- `@returns {Function}` Returns the tag template function.
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
const Btn = styled('button')`
|
||||
border-radius: 4px;
|
||||
`;
|
||||
```
|
||||
|
||||
#### Different ways of customizing the styles
|
||||
|
||||
##### Tagged templates functions
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
const Btn = styled('button')`
|
||||
border-radius: ${(props) => props.size}px;
|
||||
`;
|
||||
|
||||
<Btn size={20} />;
|
||||
```
|
||||
|
||||
##### Function that returns a string
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
const Btn = styled('button')(
|
||||
(props) => `
|
||||
border-radius: ${props.size}px;
|
||||
`
|
||||
);
|
||||
|
||||
<Btn size={20} />;
|
||||
```
|
||||
|
||||
##### JSON/Object
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
const Btn = styled('button')((props) => ({
|
||||
borderRadius: props.size + 'px'
|
||||
}));
|
||||
|
||||
<Btn size={20} />;
|
||||
```
|
||||
|
||||
##### Arrays
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
const Btn = styled('button')([
|
||||
{ color: 'tomato' },
|
||||
({ isPrimary }) => ({ background: isPrimary ? 'cyan' : 'gray' })
|
||||
]);
|
||||
|
||||
<Btn />; // This will render the `Button` with `background: gray;`
|
||||
<Btn isPrimary />; // This will render the `Button` with `background: cyan;`
|
||||
```
|
||||
|
||||
##### Forward ref function
|
||||
|
||||
As goober is JSX library agnostic, you need to pass in the forward ref function for the library you are using. Here's how you do it for React.
|
||||
|
||||
```js
|
||||
const Title = styled('h1', React.forwardRef)`
|
||||
font-weight: bold;
|
||||
color: dodgerblue;
|
||||
`;
|
||||
```
|
||||
|
||||
### `setup(pragma: Function, prefixer?: Function, theme?: Function, forwardProps?: Function)`
|
||||
|
||||
The call to `setup()` should occur only once. It should be called in the entry file of your project.
|
||||
|
||||
Given the fact that `react` uses `createElement` for the transformed elements and `preact` uses `h`, `setup` should be called with the proper _pragma_ function. This was added to reduce the bundled size and being able to bundle an esmodule version. At the moment, it's the best tradeoff I can think of.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { setup } from 'goober';
|
||||
|
||||
setup(React.createElement);
|
||||
```
|
||||
|
||||
#### With prefixer
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { setup } from 'goober';
|
||||
|
||||
const customPrefixer = (key, value) => `${key}: ${value};\n`;
|
||||
|
||||
setup(React.createElement, customPrefixer);
|
||||
```
|
||||
|
||||
#### With theme
|
||||
|
||||
```js
|
||||
import React, { createContext, useContext, createElement } from 'react';
|
||||
import { setup, styled } from 'goober';
|
||||
|
||||
const theme = { primary: 'blue' };
|
||||
const ThemeContext = createContext(theme);
|
||||
const useTheme = () => useContext(ThemeContext);
|
||||
|
||||
setup(createElement, undefined, useTheme);
|
||||
|
||||
const ContainerWithTheme = styled('div')`
|
||||
color: ${(props) => props.theme.primary};
|
||||
`;
|
||||
```
|
||||
|
||||
#### With forwardProps
|
||||
|
||||
The `forwardProps` function offers a way to achieve the same `shouldForwardProps` functionality as emotion and styled-components (with transient props) offer. The difference here is that the function receives the whole props and you are in charge of removing the props that should not end up in the DOM.
|
||||
|
||||
This is a super useful functionality when paired with theme object, variants, or any other customisation one might need.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { setup, styled } from 'goober';
|
||||
|
||||
setup(React.createElement, undefined, undefined, (props) => {
|
||||
for (let prop in props) {
|
||||
// Or any other conditions.
|
||||
// This could also check if this is a dev build and not remove the props
|
||||
if (prop === 'size') {
|
||||
delete props[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The functionality of "transient props" (with a "\$" prefix) can be implemented as follows:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { setup, styled } from 'goober';
|
||||
|
||||
setup(React.createElement, undefined, undefined, (props) => {
|
||||
for (let prop in props) {
|
||||
if (prop[0] === '$') {
|
||||
delete props[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively you can use `goober/should-forward-prop` addon to pass only the filter function and not have to deal with the full `props` object.
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { setup, styled } from 'goober';
|
||||
import { shouldForwardProp } from 'goober/should-forward-prop';
|
||||
|
||||
setup(
|
||||
React.createElement,
|
||||
undefined,
|
||||
undefined,
|
||||
// This package accepts a `filter` function. If you return false that prop
|
||||
// won't be included in the forwarded props.
|
||||
shouldForwardProp((prop) => {
|
||||
return prop !== 'size';
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### `css(taggedTemplate)`
|
||||
|
||||
- `@returns {String}` Returns the className.
|
||||
|
||||
To create a className, you need to call `css` with your style rules in a tagged template.
|
||||
|
||||
```js
|
||||
import { css } from "goober";
|
||||
|
||||
const BtnClassName = css`
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
// vanilla JS
|
||||
const btn = document.querySelector("#btn");
|
||||
// BtnClassName === 'g016232'
|
||||
btn.classList.add(BtnClassName);
|
||||
|
||||
// JSX
|
||||
// BtnClassName === 'g016232'
|
||||
const App => <button className={BtnClassName}>click</button>
|
||||
```
|
||||
|
||||
#### Different ways of customizing `css`
|
||||
|
||||
##### Passing props to `css` tagged templates
|
||||
|
||||
```js
|
||||
import { css } from 'goober';
|
||||
|
||||
// JSX
|
||||
const CustomButton = (props) => (
|
||||
<button
|
||||
className={css`
|
||||
border-radius: ${props.size}px;
|
||||
`}
|
||||
>
|
||||
click
|
||||
</button>
|
||||
);
|
||||
```
|
||||
|
||||
##### Using `css` with JSON/Object
|
||||
|
||||
```js
|
||||
import { css } from 'goober';
|
||||
const BtnClassName = (props) =>
|
||||
css({
|
||||
background: props.color,
|
||||
borderRadius: props.radius + 'px'
|
||||
});
|
||||
```
|
||||
|
||||
**Notice:** using `css` with object can reduce your bundle size.
|
||||
|
||||
We can also declare styles at the top of the file by wrapping `css` into a function that we call to get the className.
|
||||
|
||||
```js
|
||||
import { css } from 'goober';
|
||||
|
||||
const BtnClassName = (props) => css`
|
||||
border-radius: ${props.size}px;
|
||||
`;
|
||||
|
||||
// vanilla JS
|
||||
// BtnClassName({size:20}) -> g016360
|
||||
const btn = document.querySelector('#btn');
|
||||
btn.classList.add(BtnClassName({ size: 20 }));
|
||||
|
||||
// JSX
|
||||
// BtnClassName({size:20}) -> g016360
|
||||
const App = () => <button className={BtnClassName({ size: 20 })}>click</button>;
|
||||
```
|
||||
|
||||
The difference between calling `css` directly and wrapping into a function is the timing of its execution. The former is when the component(file) is imported, the latter is when it is actually rendered.
|
||||
|
||||
If you use `extractCSS` for SSR, you may prefer to use the latter, or the `styled` API to avoid inconsistent results.
|
||||
|
||||
### `targets`
|
||||
|
||||
By default, goober will append a style tag to the `<head>` of a document. You might want to target a different node, for instance, when you want to use goober with web components (so you'd want it to append style tags to individual shadowRoots). For this purpose, you can `.bind` a new target to the `styled` and `css` methods:
|
||||
|
||||
```js
|
||||
import * as goober from 'goober';
|
||||
const target = document.getElementById('target');
|
||||
const css = goober.css.bind({ target: target });
|
||||
const styled = goober.styled.bind({ target: target });
|
||||
```
|
||||
|
||||
If you don't provide a target, goober always defaults to `<head>` and in environments without a DOM (think certain SSR solutions), it will just use a plain string cache to store generated styles which you can extract with `extractCSS`(see below).
|
||||
|
||||
### `extractCss(target?)`
|
||||
|
||||
- `@returns {String}`
|
||||
|
||||
Returns the `<style>` tag that is rendered in a target and clears the style sheet. Defaults to `<head>`.
|
||||
|
||||
```js
|
||||
const { extractCss } = require('goober');
|
||||
|
||||
// After your app has rendered, just call it:
|
||||
const styleTag = `<style id="_goober">${extractCss()}</style>`;
|
||||
|
||||
// Note: To be able to `hydrate` the styles you should use the proper `id` so `goober` can pick it up and use it as the target from now on
|
||||
```
|
||||
|
||||
### `createGlobalStyles`
|
||||
|
||||
To define your global styles you need to create a `GlobalStyles` component and use it as part of your tree. The `createGlobalStyles` is available at `goober/global` addon.
|
||||
|
||||
```js
|
||||
import { createGlobalStyles } from 'goober/global';
|
||||
|
||||
const GlobalStyles = createGlobalStyles`
|
||||
html,
|
||||
body {
|
||||
background: light;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div id="root">
|
||||
<GlobalStyles />
|
||||
<Navigation>
|
||||
<RestOfYourApp>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### How about using `glob` function directly?
|
||||
|
||||
Before the global addon, `goober/global`, there was a method named `glob` that was part of the main package that would do the same thing, more or less. Having only that method to define global styles usually led to missing global styles from the extracted css, since the pattern did not enforce the evaluation of the styles at render time. The `glob` method is still exported from `goober/global`, in case you have a hard dependency on it. It still has the same API:
|
||||
|
||||
```js
|
||||
import { glob } from 'goober';
|
||||
|
||||
glob`
|
||||
html,
|
||||
body {
|
||||
background: light;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
### `keyframes`
|
||||
|
||||
`keyframes` is a helpful method to define reusable animations that can be decoupled from the main style declaration and shared across components.
|
||||
|
||||
```js
|
||||
import { keyframes } from 'goober';
|
||||
|
||||
const rotate = keyframes`
|
||||
from, to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Wicked = styled('div')`
|
||||
background: tomato;
|
||||
color: white;
|
||||
animation: ${rotate} 1s ease-in-out;
|
||||
`;
|
||||
```
|
||||
|
||||
### `shouldForwardProp`
|
||||
|
||||
To implement the `shouldForwardProp` without the need to provide the full loop over `props` you can use the `goober/should-forward-prop` addon.
|
||||
|
||||
```js
|
||||
import { h } from 'preact';
|
||||
import { setup } from 'goober';
|
||||
import { shouldForwardProp } from 'goober/should-forward-prop';
|
||||
|
||||
setup(
|
||||
h,
|
||||
undefined,
|
||||
undefined,
|
||||
shouldForwardProp((prop) => {
|
||||
// Do NOT forward props that start with `$` symbol
|
||||
return prop['0'] !== '$';
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
# Integrations
|
||||
|
||||
## Babel plugin
|
||||
|
||||
You're in love with the `styled.div` syntax? Fear no more! We got you covered with a babel plugin that will take your lovely syntax from `styled.tag` and translate it to goober's `styled("tag")` call.
|
||||
|
||||
```sh
|
||||
npm i --save-dev babel-plugin-transform-goober
|
||||
# or
|
||||
yarn add --dev babel-plugin-transform-goober
|
||||
```
|
||||
|
||||
Visit the package in here for more info (https://github.com/cristianbote/goober/tree/master/packages/babel-plugin-transform-goober)
|
||||
|
||||
## Babel macro plugin
|
||||
|
||||
A babel-plugin-macros macro for [🥜goober][goober], rewriting `styled.div` syntax to `styled('div')` calls.
|
||||
|
||||
### Usage
|
||||
|
||||
Once you've configured [babel-plugin-macros](https://github.com/kentcdodds/babel-plugin-macros), change your imports from `goober` to `goober/macro`.
|
||||
|
||||
Now you can create your components using `styled.*` syntax:.
|
||||
|
||||
```js
|
||||
import { styled } from 'goober/macro';
|
||||
|
||||
const Button = styled.button`
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
background-color: tomato;
|
||||
`;
|
||||
```
|
||||
|
||||
## [Next.js](https://github.com/vercel/next.js)
|
||||
|
||||
Want to use `goober` with Next.js? We've got you covered! Follow the example below or from the main [examples](https://github.com/vercel/next.js/tree/canary/examples/with-goober) directory.
|
||||
|
||||
```sh
|
||||
npx create-next-app --example with-goober with-goober-app
|
||||
# or
|
||||
yarn create next-app --example with-goober with-goober-app
|
||||
```
|
||||
|
||||
## [Gatsby](https://github.com/gatsbyjs/gatsby)
|
||||
|
||||
Want to use `goober` with Gatsby? We've got you covered! We have our own plugin to deal with styling your Gatsby projects.
|
||||
|
||||
```sh
|
||||
npm i --save goober gatsby-plugin-goober
|
||||
# or
|
||||
yarn add goober gatsby-plugin-goober
|
||||
```
|
||||
|
||||
## Preact CLI plugin
|
||||
|
||||
If you use Goober with Preact CLI, you can use [preact-cli-goober-ssr](https://github.com/gerhardsletten/preact-cli-goober-ssr)
|
||||
|
||||
```sh
|
||||
npm i --save-dev preact-cli-goober-ssr
|
||||
# or
|
||||
yarn add --dev preact-cli-goober-ssr
|
||||
|
||||
# preact.config.js
|
||||
const gooberPlugin = require('preact-cli-goober-ssr')
|
||||
|
||||
export default (config, env) => {
|
||||
gooberPlugin(config, env)
|
||||
}
|
||||
```
|
||||
|
||||
When you build your Preact application, this will run `extractCss` on your pre-rendered pages and add critical styles for each page.
|
||||
|
||||
## CSS Prop
|
||||
|
||||
You can use a custom `css` prop to pass in styles on HTML elements with this Babel plugin.
|
||||
|
||||
Installation:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @agney/babel-plugin-goober-css-prop
|
||||
```
|
||||
|
||||
List the plugin in `.babelrc`:
|
||||
|
||||
```
|
||||
{
|
||||
"plugins": [
|
||||
"@agney/babel-plugin-goober-css-prop"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
<main
|
||||
css={`
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<h1 css="color: dodgerblue">Goober</h1>
|
||||
</main>
|
||||
```
|
||||
|
||||
# Features
|
||||
|
||||
- [x] Basic CSS parsing
|
||||
- [x] Nested rules with pseudo selectors
|
||||
- [x] Nested styled components
|
||||
- [x] [Extending Styles](#sharing-style)
|
||||
- [x] Media queries (@media)
|
||||
- [x] Keyframes (@keyframes)
|
||||
- [x] Smart (lazy) client-side hydration
|
||||
- [x] Styling any component
|
||||
- via `` const Btn = ({className}) => {...}; const TomatoBtn = styled(Btn)`color: tomato;` ``
|
||||
- [x] Vanilla (via `css` function)
|
||||
- [x] `globalStyle` (via `glob`) so one would be able to create global styles
|
||||
- [x] target/extract from elements other than `<head>`
|
||||
- [x] [vendor prefixing](#autoprefixer)
|
||||
|
||||
# Content Security Policy (CSP)
|
||||
|
||||
goober supports Content Security Policy nonces for inline styles. Set `window.__nonce__` before loading the library:
|
||||
|
||||
```js
|
||||
<script nonce="your-nonce-here">
|
||||
window.__nonce__ = 'your-nonce-here';
|
||||
</script>
|
||||
```
|
||||
|
||||
The nonce will be added to goober's `<style>` element.
|
||||
|
||||
# Sharing style
|
||||
|
||||
There are a couple of ways to effectively share/extend styles across components.
|
||||
|
||||
## Extending
|
||||
|
||||
You can extend the desired component that needs to be enriched or overwritten with another set of css rules.
|
||||
|
||||
```js
|
||||
import { styled } from 'goober';
|
||||
|
||||
// Let's declare a primitive for our styled component
|
||||
const Primitive = styled('span')`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
// Later on we could get the primitive shared styles and also add our owns
|
||||
const Container = styled(Primitive)`
|
||||
padding: 1em;
|
||||
`;
|
||||
```
|
||||
|
||||
## Using `as` prop
|
||||
|
||||
Another helpful way to extend a certain component is with the `as` property. Given our example above we could modify it like:
|
||||
|
||||
```jsx
|
||||
import { styled } from 'goober';
|
||||
|
||||
// Our primitive element
|
||||
const Primitive = styled('span')`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const Container = styled('div')`
|
||||
padding: 1em;
|
||||
`;
|
||||
|
||||
// At composition/render time
|
||||
<Primitive as={'div'} /> // <div class="go01234" />
|
||||
|
||||
// Or using the `Container`
|
||||
<Primitive as={Container} /> // <div class="go01234 go56789" />
|
||||
```
|
||||
|
||||
# Autoprefixer
|
||||
|
||||
Autoprefixing is a helpful way to make sure the generated css will work seamlessly on the whole spectrum of browsers. With that in mind, the core `goober` package can't hold that logic to determine the autoprefixing needs, so we added a new package that you can choose to address them.
|
||||
|
||||
```sh
|
||||
npm install goober
|
||||
# or
|
||||
yarn add goober
|
||||
```
|
||||
|
||||
After the main package is installed it's time to bootstrap goober with it:
|
||||
|
||||
```js
|
||||
import { setup } from 'goober';
|
||||
import { prefix } from 'goober/prefixer';
|
||||
|
||||
// Bootstrap goober
|
||||
setup(React.createElement, prefix);
|
||||
```
|
||||
|
||||
And voilà! It is done!
|
||||
|
||||
# TypeScript
|
||||
|
||||
`goober` comes with type definitions build in, making it easy to get started in TypeScript straight away.
|
||||
|
||||
## Prop Types
|
||||
|
||||
If you're using custom props and wish to style based on them, you can do so as follows:
|
||||
|
||||
```ts
|
||||
interface Props {
|
||||
size: number;
|
||||
}
|
||||
|
||||
styled('div')<Props>`
|
||||
border-radius: ${(props) => props.size}px;
|
||||
`;
|
||||
|
||||
// This also works!
|
||||
|
||||
styled<Props>('div')`
|
||||
border-radius: ${(props) => props.size}px;
|
||||
`;
|
||||
```
|
||||
|
||||
## Extending Theme
|
||||
|
||||
If you're using a [custom theme](../api/setup.md#with-theme) and want to add types to it, you can create a declaration file at the base of your project.
|
||||
|
||||
```ts
|
||||
// goober.d.t.s
|
||||
|
||||
import 'goober';
|
||||
|
||||
declare module 'goober' {
|
||||
export interface DefaultTheme {
|
||||
colors: {
|
||||
primary: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should now have autocompletion for your theme.
|
||||
|
||||
```ts
|
||||
const ThemeContainer = styled('div')`
|
||||
background-color: ${(props) => props.theme.colors.primary};
|
||||
`;
|
||||
```
|
||||
|
||||
# Browser support
|
||||
|
||||
`goober` supports all major browsers (Chrome, Edge, Firefox, Safari).
|
||||
|
||||
To support IE 11 and older browsers, make sure to use a tool like [Babel](https://babeljs.io/) to transform your code into code that works in the browsers you target.
|
||||
|
||||
# Contributing
|
||||
|
||||
Feel free to try it out and checkout the examples. If you wanna fix something feel free to open a issue or a PR.
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏
|
||||
<a href="https://opencollective.com/goober#backers" target="_blank"><img src="https://opencollective.com/goober/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
|
||||
<a href="https://opencollective.com/goober#sponsors" target="_blank"><img src="https://opencollective.com/goober/sponsors.svg?width=890"></a>
|
||||
|
|
@ -0,0 +1 @@
|
|||
let e={data:""},t=t=>{if("object"==typeof window){let e=(t?t.querySelector("#_goober"):window._goober)||Object.assign(document.createElement("style"),{innerHTML:" ",id:"_goober"});return e.nonce=window.__nonce__,e.parentNode||(t||document.head).appendChild(e),e.firstChild}return t||e},r=/(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g,l=/\/\*[^]*?\*\/| +/g,a=/\n+/g,n=(e,t)=>{let r="",l="",a="";for(let o in e){let s=e[o];"@"==o[0]?"i"==o[1]?r=o+" "+s+";":l+="f"==o[1]?n(s,o):o+"{"+n(s,"k"==o[1]?"":t)+"}":"object"==typeof s?l+=n(s,t?t.replace(/([^,])+/g,e=>o.replace(/([^,]*:\S+\([^)]*\))|([^,])+/g,t=>/&/.test(t)?t.replace(/&/g,e):e?e+" "+t:t)):o):null!=s&&(o=/^--/.test(o)?o:o.replace(/[A-Z]/g,"-$&").toLowerCase(),a+=n.p?n.p(o,s):o+":"+s+";")}return r+(t&&a?t+"{"+a+"}":a)+l},o={},s=e=>{if("object"==typeof e){let t="";for(let r in e)t+=r+s(e[r]);return t}return e},c=(e,t,c,p,i)=>{let u=s(e),d=o[u]||(o[u]=(e=>{let t=0,r=11;for(;t<e.length;)r=101*r+e.charCodeAt(t++)>>>0;return"go"+r})(u));if(!o[d]){let t=u!==e?e:(e=>{let t,n,o=[{}];for(;t=r.exec(e.replace(l,""));)t[4]?o.shift():t[3]?(n=t[3].replace(a," ").trim(),o.unshift(o[0][n]=o[0][n]||{})):o[0][t[1]]=t[2].replace(a," ").trim();return o[0]})(e);o[d]=n(i?{["@keyframes "+d]:t}:t,c?"":"."+d)}let f=c&&o.g?o.g:null;return c&&(o.g=o[d]),((e,t,r,l)=>{l?t.data=t.data.replace(l,e):-1===t.data.indexOf(e)&&(t.data=r?e+t.data:t.data+e)})(o[d],t,p,f),d},p=(e,t,r)=>e.reduce((e,l,a)=>{let o=t[a];if(o&&o.call){let e=o(r),t=e&&e.props&&e.props.className||/^go/.test(e)&&e;o=t?"."+t:e&&"object"==typeof e?e.props?"":n(e,""):!1===e?"":e}return e+l+(null==o?"":o)},"");function i(e){let r=this||{},l=e.call?e(r.p):e;return c(l.unshift?l.raw?p(l,[].slice.call(arguments,1),r.p):l.reduce((e,t)=>Object.assign(e,t&&t.call?t(r.p):t),{}):l,t(r.target),r.g,r.o,r.k)}let u,d,f,g=i.bind({g:1}),b=i.bind({k:1});exports.css=i,exports.extractCss=e=>{let r=t(e),l=r.data;return r.data="",l},exports.glob=g,exports.keyframes=b,exports.setup=function(e,t,r,l){n.p=t,u=e,d=r,f=l},exports.styled=function(e,t){let r=this||{};return function(){let l=arguments;function a(n,o){let s=Object.assign({},n),c=s.className||a.className;r.p=Object.assign({theme:d&&d()},s),r.o=/ *go\d+/.test(c),s.className=i.apply(r,l)+(c?" "+c:""),t&&(s.ref=o);let p=e;return e[0]&&(p=s.as||e,delete s.as),f&&p[0]&&f(s),u(p,s)}return t?t(a):a}};
|
||||
|
|
@ -0,0 +1 @@
|
|||
let e={data:""},t=t=>{if("object"==typeof window){let e=(t?t.querySelector("#_goober"):window._goober)||Object.assign(document.createElement("style"),{innerHTML:" ",id:"_goober"});return e.nonce=window.__nonce__,e.parentNode||(t||document.head).appendChild(e),e.firstChild}return t||e},r=e=>{let r=t(e),l=r.data;return r.data="",l},l=/(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g,a=/\/\*[^]*?\*\/| +/g,n=/\n+/g,o=(e,t)=>{let r="",l="",a="";for(let n in e){let c=e[n];"@"==n[0]?"i"==n[1]?r=n+" "+c+";":l+="f"==n[1]?o(c,n):n+"{"+o(c,"k"==n[1]?"":t)+"}":"object"==typeof c?l+=o(c,t?t.replace(/([^,])+/g,e=>n.replace(/([^,]*:\S+\([^)]*\))|([^,])+/g,t=>/&/.test(t)?t.replace(/&/g,e):e?e+" "+t:t)):n):null!=c&&(n=/^--/.test(n)?n:n.replace(/[A-Z]/g,"-$&").toLowerCase(),a+=o.p?o.p(n,c):n+":"+c+";")}return r+(t&&a?t+"{"+a+"}":a)+l},c={},s=e=>{if("object"==typeof e){let t="";for(let r in e)t+=r+s(e[r]);return t}return e},i=(e,t,r,i,p)=>{let u=s(e),d=c[u]||(c[u]=(e=>{let t=0,r=11;for(;t<e.length;)r=101*r+e.charCodeAt(t++)>>>0;return"go"+r})(u));if(!c[d]){let t=u!==e?e:(e=>{let t,r,o=[{}];for(;t=l.exec(e.replace(a,""));)t[4]?o.shift():t[3]?(r=t[3].replace(n," ").trim(),o.unshift(o[0][r]=o[0][r]||{})):o[0][t[1]]=t[2].replace(n," ").trim();return o[0]})(e);c[d]=o(p?{["@keyframes "+d]:t}:t,r?"":"."+d)}let f=r&&c.g?c.g:null;return r&&(c.g=c[d]),((e,t,r,l)=>{l?t.data=t.data.replace(l,e):-1===t.data.indexOf(e)&&(t.data=r?e+t.data:t.data+e)})(c[d],t,i,f),d},p=(e,t,r)=>e.reduce((e,l,a)=>{let n=t[a];if(n&&n.call){let e=n(r),t=e&&e.props&&e.props.className||/^go/.test(e)&&e;n=t?"."+t:e&&"object"==typeof e?e.props?"":o(e,""):!1===e?"":e}return e+l+(null==n?"":n)},"");function u(e){let r=this||{},l=e.call?e(r.p):e;return i(l.unshift?l.raw?p(l,[].slice.call(arguments,1),r.p):l.reduce((e,t)=>Object.assign(e,t&&t.call?t(r.p):t),{}):l,t(r.target),r.g,r.o,r.k)}let d,f,g,b=u.bind({g:1}),h=u.bind({k:1});function m(e,t,r,l){o.p=t,d=e,f=r,g=l}function w(e,t){let r=this||{};return function(){let l=arguments;function a(n,o){let c=Object.assign({},n),s=c.className||a.className;r.p=Object.assign({theme:f&&f()},c),r.o=/ *go\d+/.test(s),c.className=u.apply(r,l)+(s?" "+s:""),t&&(c.ref=o);let i=e;return e[0]&&(i=c.as||e,delete c.as),g&&i[0]&&g(c),d(i,c)}return t?t(a):a}}export{u as css,r as extractCss,b as glob,h as keyframes,m as setup,w as styled};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue