a982795388
KRITISCH: - emails/:id/thread bekommt canAccessCachedEmail - customers/:customerId/representatives/search bekommt canAccessCustomer (Buchstaben-Brute-Force konnte sonst die Kunden-DB enumerieren) HOCH: - birthdays/upcoming: Portal-User → 403 (Name/E-Mail/Telefon/Geb-Datum aller Kunden leakte) - contracts/:id/history (GET/POST/PUT/DELETE) bekommt canAccessContract - mailbox-accounts / unread-count / contracts/:id/emails/folder-counts bekommen canAccessCustomer bzw. canAccessContract - Vertreter-Vollmacht-Check ist jetzt live: neuer Helper getPortalAllowedCustomerIds() in accessControl.ts ruft hasAuthorization() für jedes vertretene Customer ab. Eingesetzt in getTasks/createSupportTicket/createCustomerReply/getAllTasks/ getTaskStats und updateCustomerConsent. Widerrufene Vollmachten haben jetzt SOFORT keinen Zugriff mehr (vorher: bis JWT abläuft). MITTEL: - confirmPasswordReset speichert portalPasswordEncrypted nicht mehr beim Self-Service-Reset (war nur für Admin-OTPs gedacht); + portalPasswordMustChange=false explizit - getCustomers pagination total reflektiert jetzt nur erlaubte IDs (über DB-Filter in customerService.getAllCustomers) Audit-Sweep (defense in depth, falls Rolle versehentlich Update- Permissions bekommt): - 16 cachedEmail-Operationen (markAsRead, toggleStar, assign/unassign, save-as-pdf/invoice/contract-document, save-to, attachment-targets, trash-ops) - 4 contract-Operationen (createFollowUp, createRenewal, snoozeContract, removeContractMeter) - 12 sub-CRUD-Operationen (address/bankcard/document/meter update+delete, meter-reading add/update/delete/transfer) - 2 representative-Operationen (add/remove) Live-verifiziert: Portal-Customer-3 auf alle fremden IDs → 403, Admin sieht alles, eigene Ressourcen weiterhin 200, Customer 1 mit widerrufener Vollmacht für Customer 3 → 0 fremde Verträge in der Response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
import { Response } from 'express';
|
||
import { AuthRequest } from '../types/index.js';
|
||
import * as birthdayService from '../services/birthday.service.js';
|
||
import { sendEmail, SmtpCredentials } from '../services/smtpService.js';
|
||
import { getSystemEmailCredentials } from '../services/emailProvider/emailProviderService.js';
|
||
import { createAuditLog } from '../services/audit.service.js';
|
||
|
||
/**
|
||
* Admin/Mitarbeiter: Kommende und vergangene Geburtstage
|
||
* Query: ?past=7&future=30 (Default)
|
||
*/
|
||
export async function getUpcomingBirthdays(req: AuthRequest, res: Response) {
|
||
try {
|
||
// Portal-Kunden haben hier nichts zu suchen. Endpoint listet Namen, E-Mail,
|
||
// Telefon und Geburtsdatum ALLER Kunden – ausschließlich Mitarbeiter-UI.
|
||
// Pentest Runde 6 (2026-05-16) – HOCH.
|
||
if (req.user?.isCustomerPortal) {
|
||
res.status(403).json({ success: false, error: 'Nicht erlaubt' });
|
||
return;
|
||
}
|
||
const past = req.query.past ? parseInt(String(req.query.past)) : 7;
|
||
const future = req.query.future ? parseInt(String(req.query.future)) : 30;
|
||
|
||
const entries = await birthdayService.getUpcomingBirthdays(past, future);
|
||
res.json({ success: true, data: entries });
|
||
} catch (error) {
|
||
console.error('Fehler beim Abrufen der Geburtstage:', error);
|
||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Geburtstage' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Portal: Eigenen Geburtstags-Check – soll das Modal angezeigt werden?
|
||
*/
|
||
export async function getMyBirthday(req: AuthRequest, res: Response) {
|
||
try {
|
||
const user = req.user as any;
|
||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||
}
|
||
|
||
const data = await birthdayService.checkMyBirthday(user.customerId);
|
||
res.json({ success: true, data });
|
||
} catch (error) {
|
||
console.error('Fehler beim Geburtstags-Check:', error);
|
||
res.status(500).json({ success: false, error: 'Fehler beim Abruf' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Portal: Modal als gesehen markieren
|
||
*/
|
||
export async function acknowledgeMyBirthday(req: AuthRequest, res: Response) {
|
||
try {
|
||
const user = req.user as any;
|
||
if (!user?.isCustomerPortal || !user?.customerId) {
|
||
return res.status(403).json({ success: false, error: 'Nur für Kundenportal-Benutzer' });
|
||
}
|
||
|
||
await birthdayService.acknowledgeBirthdayGreeting(user.customerId);
|
||
res.json({ success: true });
|
||
} catch (error) {
|
||
console.error('Fehler beim Bestätigen:', error);
|
||
res.status(500).json({ success: false, error: 'Fehler beim Speichern' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Admin: Geburtstagsgruß-Marker für einen Kunden zurücksetzen (Debug / Re-Trigger).
|
||
*/
|
||
export async function resetBirthdayGreeting(req: AuthRequest, res: Response) {
|
||
try {
|
||
const customerId = parseInt(req.params.customerId);
|
||
await birthdayService.resetBirthdayGreeting(customerId);
|
||
|
||
await createAuditLog({
|
||
userId: req.user?.userId,
|
||
userEmail: req.user?.email || 'unknown',
|
||
action: 'UPDATE',
|
||
resourceType: 'Customer',
|
||
resourceId: customerId.toString(),
|
||
resourceLabel: `Geburtstagsgruß-Marker zurückgesetzt`,
|
||
endpoint: req.path,
|
||
httpMethod: req.method,
|
||
ipAddress: req.socket.remoteAddress || 'unknown',
|
||
dataSubjectId: customerId,
|
||
});
|
||
|
||
res.json({ success: true });
|
||
} catch (error) {
|
||
console.error('Fehler beim Zurücksetzen:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Zurücksetzen',
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Admin: Geburtstagsgruß manuell senden (Email oder Link für WhatsApp/Telegram/Signal).
|
||
*/
|
||
export async function sendBirthdayGreeting(req: AuthRequest, res: Response) {
|
||
try {
|
||
const customerId = parseInt(req.params.customerId);
|
||
const { channel } = req.body; // 'email', 'whatsapp', 'telegram', 'signal'
|
||
|
||
if (!['email', 'whatsapp', 'telegram', 'signal'].includes(channel)) {
|
||
return res.status(400).json({ success: false, error: 'Ungültiger Kanal' });
|
||
}
|
||
|
||
const data = await birthdayService.getBirthdayGreetingData(customerId);
|
||
if (!data) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Kunde hat kein Geburtsdatum hinterlegt',
|
||
});
|
||
}
|
||
|
||
const { subject, plain, html } = birthdayService.buildBirthdayGreetingText(data, data.age);
|
||
|
||
if (channel === 'email') {
|
||
if (!data.email) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Kunde hat keine E-Mail-Adresse hinterlegt',
|
||
});
|
||
}
|
||
|
||
const systemEmail = await getSystemEmailCredentials();
|
||
if (!systemEmail) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'Keine System-E-Mail konfiguriert. Bitte in den Email-Provider-Einstellungen hinterlegen.',
|
||
});
|
||
}
|
||
|
||
const credentials: SmtpCredentials = {
|
||
host: systemEmail.smtpServer,
|
||
port: systemEmail.smtpPort,
|
||
user: systemEmail.emailAddress,
|
||
password: systemEmail.password,
|
||
encryption: systemEmail.smtpEncryption,
|
||
allowSelfSignedCerts: systemEmail.allowSelfSignedCerts,
|
||
};
|
||
|
||
const result = await sendEmail(credentials, systemEmail.emailAddress, {
|
||
to: data.email,
|
||
subject,
|
||
html,
|
||
}, {
|
||
context: 'birthday-greeting',
|
||
customerId,
|
||
triggeredBy: req.user?.email,
|
||
});
|
||
|
||
if (!result.success) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: `E-Mail-Versand fehlgeschlagen: ${result.error}`,
|
||
});
|
||
}
|
||
}
|
||
|
||
await createAuditLog({
|
||
userId: req.user?.userId,
|
||
userEmail: req.user?.email || 'unknown',
|
||
action: 'CREATE',
|
||
resourceType: 'Customer',
|
||
resourceId: customerId.toString(),
|
||
resourceLabel: `Geburtstagsgruß gesendet (${channel})`,
|
||
endpoint: req.path,
|
||
httpMethod: req.method,
|
||
ipAddress: req.socket.remoteAddress || 'unknown',
|
||
dataSubjectId: customerId,
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
data: { channel, messageText: plain },
|
||
});
|
||
} catch (error) {
|
||
console.error('Fehler beim Senden des Geburtstagsgrußes:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Fehler beim Senden',
|
||
});
|
||
}
|
||
}
|