Communications & Credentials Audit Panel — Design
DerinStatus: Approved (brainstorming complete)
Date: 2026-05-03
Owner: Admin Panel
Related security debt: memory/security_findings.md (TrialCredential plaintext)
1. Goal
Bölüm başlığı “1. Goal”Admin panele iki yetenek eklemek:
-
Communications log — müşteriye gönderilen tüm e-posta ve SMS iletilerinin tam içeriğini (rendered HTML, plain text, headers, template payload) kalıcı olarak loglamak ve admin UI’da forensic-level görüntüleme, filtre, yeniden gönderme, manuel silme ve CSV export yetenekleri sunmak.
-
Credential visibility — provisionlanan tüm portallar için admin login, customer login, SMTP/IMAP ve API token credential’larını hem portal-bazında (subdomain × credential matrix) hem tip-bazında (cross-portal admin/customer/mail/API listeleri) görünür kılmak. Reveal/copy aksiyonları audit log’a yazılır.
İki yetenek aynı admin layout altında, paylaşılan audit ve crypto altyapısını kullanarak tek spec olarak modellenir.
2. Non-Goals
Bölüm başlığı “2. Non-Goals”- TrialCredential plaintext alanlarını AES-256-GCM ile şifrelemek (ayrı security spec’inde ele alınacak — security debt listesinde kayıtlı).
- Müşteri tarafından alınan (inbound) e-posta veya SMS’leri loglamak — sadece giden iletişim.
- Mevcut
EmailQueueretry mekanizmasını yeniden yazmak — log entegrasyonu sadece referans (emailLogId) ekler. - 2FA reauth gibi reveal-time friction — perimetre auth (admin login) yeterli kabul edilir, audit log post-mortem güvenliği sağlar.
- AdminUser role/permission sistemi — şu an tüm AdminUser eşit yetkiye sahip, scope dışı.
3. Architecture Overview
Bölüm başlığı “3. Architecture Overview”┌─────────────────────────────────────────────────────────────────┐│ Existing call sites (~30 mail + ~5 SMS) — değişmez API ││ sendMail({...}) / sendSms({...}) │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ lib/mail/service.ts / lib/sms/service.ts ││ 1. Render template ││ 2. INSERT EmailLog/SmsLog (status: pending) ││ 3. Encrypt payload data → MailPayload/SmsPayload ││ 4. Transport.send() ││ 5. UPDATE EmailLog/SmsLog (status, messageId, error) │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ PostgreSQL ││ • EmailLog (full content, TOAST-compressed) ││ • SmsLog ││ • MailPayload / SmsPayload (AES-256-GCM ciphertext) ││ • AdminAuditLog (mevcut — yeni action enum değerleri) │└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐│ Admin UI (mevcut admin layout altında) ││ /admin/communications ││ ├── /emails liste + filtre + detay drawer ││ ├── /sms liste + filtre + detay drawer ││ /admin/credentials ││ ├── /by-portal subdomain listesi ││ │ └── /[subdomain] 4 credential bloğu (admin/cust/mail/api) ││ └── /by-type /admins, /customers, /mail, /api │└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐│ Cron — /api/cron/log-retention (günde 1) ││ 90 günden eski EmailLog/SmsLog + payload satırlarını siler │└─────────────────────────────────────────────────────────────────┘4. Data Model
Bölüm başlığı “4. Data Model”4.1 Yeni Prisma modelleri
Bölüm başlığı “4.1 Yeni Prisma modelleri”model EmailLog { id Int @id @default(autoincrement()) template String // 'magic-link', 'trial-ready' vb. recipient String // virgülle ayrılmış (multi-recipient) subject String html String @db.Text // PostgreSQL TOAST + LZ4 text String @db.Text headers Json // List-Unsubscribe, Message-ID dahil locale String? status String @default("pending") // pending | sent | failed | retrying messageId String? // SMTP message-id errorMessage String? attemptCount Int @default(1) customerId Int? adminId Int? ipAddress String? createdAt DateTime @default(now()) sentAt DateTime?
payload MailPayload?
@@index([template]) @@index([recipient]) @@index([customerId]) @@index([status]) @@index([createdAt])}
model MailPayload { emailLogId Int @id ciphertext Bytes // AES-256-GCM(JSON.stringify(data)) iv Bytes authTag Bytes emailLog EmailLog @relation(fields: [emailLogId], references: [id], onDelete: Cascade)}
model SmsLog { id Int @id @default(autoincrement()) template String // 'otp', 'login-alert' vb. recipient String // E.164 body String @db.Text fromNumber String? locale String? status String @default("pending") // pending | sent | failed twilioSid String? errorMessage String? customerId Int? adminId Int? ipAddress String? createdAt DateTime @default(now()) sentAt DateTime?
payload SmsPayload?
@@index([template]) @@index([recipient]) @@index([customerId]) @@index([status]) @@index([createdAt])}
model SmsPayload { smsLogId Int @id ciphertext Bytes iv Bytes authTag Bytes smsLog SmsLog @relation(fields: [smsLogId], references: [id], onDelete: Cascade)}4.2 Crypto
Bölüm başlığı “4.2 Crypto”lib/crypto/payload-cipher.ts — AES-256-GCM, MAIL_PAYLOAD_KEY env (32-byte hex). PasswordEntry cipher pattern’i ile aynı API ama ayrı key (PII vs. credential ayrımı).
export function encryptPayload(plaintext: string): { ciphertext: Buffer; iv: Buffer; authTag: Buffer };export function decryptPayload(c: { ciphertext: Buffer; iv: Buffer; authTag: Buffer }): string;4.3 Mevcut tablolar — değişmez
Bölüm başlığı “4.3 Mevcut tablolar — değişmez”TrialCredential— okunur, yazılmaz. Plaintext alanlar olduğu gibi kalır.PortalInstallation—TrialCredentialile JOIN edilir (subdomain üzerinden).AdminAuditLog— yeniactionstring değerleri (migration gerekmez, string column).
5. UI / Routes
Bölüm başlığı “5. UI / Routes”5.1 Sayfa hiyerarşisi
Bölüm başlığı “5.1 Sayfa hiyerarşisi”app/[locale]/admin/(panel)/├── communications/│ ├── layout.tsx shared header + tabs│ ├── page.tsx redirect → /emails│ ├── emails/│ │ ├── page.tsx liste (server component)│ │ ├── components/│ │ │ ├── email-log-table.tsx│ │ │ ├── email-log-filters.tsx│ │ │ └── email-detail-drawer.tsx│ │ └── [id]/│ │ ├── page.tsx deep-link standalone detay│ │ └── preview/route.tsx sandboxed iframe srcdoc│ └── sms/│ ├── page.tsx│ ├── components/{sms-log-table, sms-detail-drawer}.tsx│ └── [id]/page.tsx└── credentials/ ├── layout.tsx header + sekme bar ├── page.tsx default: by-portal sekmesi ├── by-portal/ │ ├── page.tsx portal liste tablosu │ └── [subdomain]/page.tsx portal detay (4 credential bloğu) └── by-type/ ├── admins/page.tsx ├── customers/page.tsx ├── mail/page.tsx └── api/page.tsx5.2 API endpoints
Bölüm başlığı “5.2 API endpoints”app/api/admin/├── communications/│ ├── emails/│ │ ├── route.ts GET list (cursor pagination)│ │ ├── [id]/│ │ │ ├── route.ts GET detail, DELETE single│ │ │ ├── payload/route.ts GET decrypted payload (audit'e yazar)│ │ │ ├── preview/route.ts GET sandboxed HTML│ │ │ └── resend/route.ts POST tekrar gönder (sebep zorunlu)│ │ └── export/route.ts GET CSV/JSON│ └── sms/{aynı yapı}└── credentials/ ├── portals/route.ts GET list ├── portals/[subdomain]/route.ts GET tek portal'ın tüm credential'ları ├── by-type/{admins,customers,mail,api}/route.ts └── reveal/route.ts POST { credentialId, field, source }5.3 Davranışsal kurallar
Bölüm başlığı “5.3 Davranışsal kurallar”- Liste sayfaları: server-side filtering, cursor pagination (25 satır/sayfa). Filtre alanları: template, status, locale, date range, recipient (LIKE), full-text body search (opsiyonel, Phase 5).
- Detail drawer: sağdan slide, 4 tab (Rendered / Plain Text / Headers / Payload). Rendered tab
<iframe srcdoc={html} sandbox="">— hiçbir flag yok (noallow-scripts, noallow-same-origin). Bu kombinasyon iframe’i opaque origin’e koyar; script çalışmaz, parent DOM/cookie/storage erişimi yok, form post engellenir.allow-same-originparent’a erişim verdiği için kullanılmaz. - Reveal/Copy: tek endpoint,
source: 'view' | 'copy'parametresi ile ayrım yapılır.POST /api/admin/credentials/reveal { credentialId, field, source }veyaPOST /api/admin/communications/{kind}/{id}/payload { source }. Serversourcedeğerine görecredential.revealveyacredential.copyaudit kaydı yazıp plaintext döner. Frontendviewmodunda 30 saniye sonra otomatik mask + countdown bar gösterir;copymodunda plaintext clipboard’a yazılır ve UI’da gösterilmez. - Resend: zorunlu sebep alanı, yeni
EmailLogsatırı oluşturur (eskisini değiştirmez), auditcommunications.email.resend+target: emailLog:OLD → emailLog:NEW. - Delete: hem detail drawer hem context menu, audit
communications.email.delete. - Export: GET stream (CSV veya JSON), filtre parametreleri reuse, audit
communications.email.exportile satır sayısı.
6. Logging Integration
Bölüm başlığı “6. Logging Integration”6.1 sendMail değişiklikleri
Bölüm başlığı “6.1 sendMail değişiklikleri”lib/mail/service.ts sendMail fonksiyonu opsiyonel context parametresi alır:
export async function sendMail<T extends keyof TemplatePropsMap>(options: { to: string | string[]; template: T; data: TemplatePropsMap[T]; context?: { customerId?: number; adminId?: number; ipAddress?: string };}): Promise<SendMailResult>Akış:
renderTemplate()— başarısız olursa log YAZILMAZ (içerik yok),success: falsedöner.prisma.emailLog.create({ status: 'pending', ... })— başarısız olursa transport çağrısı yapılmaz, sessiz kayıp önlenir.encryptAndStorePayload('email', log.id, options.data)— başarısız olursa log’aerrorMessage: "payload_encrypt_failed: ..."yazılır, transport yine de devam eder.transporter.sendMail({ ... })— başarılı:status: sent,messageId,sentAtupdate.- Transport hatası:
status: retrying,errorMessageset,enqueueEmail({ ..., emailLogId: log.id }). Queue retry başarılı olunca logsent’e döner; üç deneme sonrası başarısız olursafailed’e düşer.
6.2 sendSms değişiklikleri
Bölüm başlığı “6.2 sendSms değişiklikleri”lib/sms/service.ts aynı pattern, queue/retry yok. Adımlar 1-4 aynı, hata durumunda status: failed direkt set edilir.
6.3 EmailQueue entegrasyonu
Bölüm başlığı “6.3 EmailQueue entegrasyonu”lib/mail/queue.ts retry job’u emailLogId taşır. Job tamamlanınca log status ve attemptCount güncellenir. Mevcut queue retry logic’i değişmez, sadece log referansı eklenir.
6.4 Call site enrichment (phased)
Bölüm başlığı “6.4 Call site enrichment (phased)”context parametresi opsiyonel — mevcut çağrılar değişmeden çalışır, sadece customerId/adminId log’da null kalır. Phase 7’de batch enrichment yapılır:
Mail call sites (~30):
app/api/admin/auth/{magic-link,forgot-password,login}/route.ts→ adminId, ipAddressapp/api/admin/{users,trial,orders}/...→ adminId, ipAddressapp/api/payment/{confirm-sepa,confirm-crypto,webhook}/route.ts→ customerId, ipAddressapp/api/customer/{2fa,account,auth}/...→ customerId, ipAddressapp/api/cron/trial-expiry/route.ts→ source: ‘cron’app/api/contact/route.ts→ ipAddress (anonim)app/lib/{customer,admin-customer}-actions.ts→ caller’danlib/ticket-notifications.ts→ customerId/adminId duruma göre
SMS call sites (~5):
app/api/{admin,customer}/auth/sms-otp/{send,verify}/route.ts→ ilgili id, ipAddressapp/api/contact/route.ts→ ipAddress
7. Audit Log Integration
Bölüm başlığı “7. Audit Log Integration”Mevcut AdminAuditLog tablosu kullanılır (string action kolonu, migration gerekmez).
7.1 Yeni action değerleri
Bölüm başlığı “7.1 Yeni action değerleri”communications.email.list_view (dedupe: 1/session/day)communications.email.detail_viewcommunications.email.body_viewcommunications.email.payload_revealcommunications.email.resendcommunications.email.deletecommunications.email.exportcommunications.sms.{list_view, detail_view, body_view, payload_reveal, resend, delete, export}credential.portal_list_view (dedupe: 1/session/day)credential.portal_detail_viewcredential.type_list_view (dedupe: 1/session/day)credential.revealcredential.copy7.2 Util
Bölüm başlığı “7.2 Util”// lib/audit/log.ts (mevcut yoksa eklenir)await logAdminAudit({ adminId, action, target, details, ipAddress, userAgent, status: 'success' | 'failed' | 'blocked',});list_view aksiyonları için in-memory deduplication: ilk hit kaydedilir, aynı admin/aynı sayfa için 24 saat içinde tekrar yazılmaz.
7.3 Mevcut audit log UI
Bölüm başlığı “7.3 Mevcut audit log UI”/admin/security (varsa) audit log filter chip’lerine Communications ve Credentials kategorileri eklenir.
8. Retention & GDPR
Bölüm başlığı “8. Retention & GDPR”8.1 Cron job
Bölüm başlığı “8.1 Cron job”app/api/cron/log-retention/route.ts — günde 1, CRON_SECRET ile korunur.
const cutoff = new Date(Date.now() - LOG_RETENTION_DAYS * 86400000);await prisma.emailLog.deleteMany({ where: { createdAt: { lt: cutoff } } });await prisma.smsLog.deleteMany({ where: { createdAt: { lt: cutoff } } });// payload onDelete: Cascade ile otomatik silinir8.2 GDPR right-to-erasure
Bölüm başlığı “8.2 GDPR right-to-erasure”- Manuel silme: admin panelden tek satır silme (audit logged).
- Müşteri silindiğinde:
EmailLog.customerIdveSmsLog.customerIdSetNullile null’a düşer — log kalır, PII bağı kopar. - İleride aktive edilebilir: payload kısa retention (30 gün) + rendered log uzun retention (90 gün) ayrımı; schema bunu destekler.
8.3 Env
Bölüm başlığı “8.3 Env”LOG_RETENTION_DAYS=90MAIL_PAYLOAD_KEY=<32-byte hex>CRON_SECRET=<existing>9. Error Handling Matrix
Bölüm başlığı “9. Error Handling Matrix”| Senaryo | Davranış |
|---|---|
EmailLog.create başarısız (DB down) | console.error, transport çağrılmaz, success: false döner. Mail gönderilmemiş kabul edilir. |
renderTemplate başarısız | Log YAZILMAZ, success: false (içerik yok). |
| Transport başarısız | Log status: retrying, errorMessage set, queue’ya alınır. |
| Payload encrypt başarısız | Log yazılır, payload null, errorMessage: "payload_encrypt_failed: ...". Transport devam eder. |
| Reveal endpoint anonim/yetkisiz | 401, audit status: blocked. |
| Decrypt başarısız | 500, audit status: failed, kullanıcıya genel hata. |
| Resend orijinal data corrupt (decrypt başarısız) | Resend reddedilir, audit status: failed. |
| Resend başarılı | Yeni EmailLog satırı, eskisi değişmez, audit chain OLD → NEW. |
10. Performance & Capacity
Bölüm başlığı “10. Performance & Capacity”EmailLog.createek latency: ~5-15 ms (PostgreSQL local). Mevcut transport ~200-500 ms; %3-7 ek yük.- HTML body en kötü ~100KB. 1.000 mail/gün × 100KB = 100MB/gün ham; TOAST + LZ4 ile ~10-20MB/gün gerçek. 90 gün retention = ~1-2GB.
- SMS body ≤ 1.530 char (multi-segment); günde 100 SMS × 200B = 20KB/gün.
- Indexler: B-tree composite
(template, createdAt),(recipient, createdAt),(customerId, createdAt),(status, createdAt). Full-text body search için GIN index opsiyonel — Phase 5’te ihtiyaca göre.
11. Testing Strategy
Bölüm başlığı “11. Testing Strategy”| Katman | Senaryo | Araç |
|---|---|---|
| Unit | payload-cipher.ts encrypt/decrypt round-trip, yanlış key reddi | Vitest |
| Unit | Audit list_view dedup logic | Vitest |
| Integration | sendMail → EmailLog satırı, transport mock, status sent | Vitest + test DB |
| Integration | Transport hatası → retrying → queue → sent veya failed | Vitest |
| Integration | Reveal endpoint → credential.reveal audit kaydı | Vitest |
| Integration | Retention cron → 90 gün öncesi siler, payload cascade | Vitest |
| Security | iframe sandbox (sandbox=""): <script>alert()</script> payload’ı admin panelde çalışmıyor, parent.document erişimi engelli | Manuel |
| Security | Reveal endpoint anonim → 401 | curl |
| Smoke | 50+ template’in her biri için sendMail → log oluşur | Smoke harness’a ek |
12. Migration & Rollout Plan
Bölüm başlığı “12. Migration & Rollout Plan”- Prisma migration 1 —
EmailLog,SmsLog,MailPayload,SmsPayloadcreate. - Env —
MAIL_PAYLOAD_KEYüretip.env’e ekle (deploy öncesi). - Code release 1 —
lib/mail/service.ts,lib/sms/service.tslog yazımı,payload-cipher.ts. Tüm yeni iletişim log’a düşer. - Code release 2 — Admin UI sayfaları (
/communications/{emails,sms},/credentials/{by-portal,by-type}). - Code release 3 — Audit log entegrasyonu, reveal endpoint, copy/delete/resend aksiyonları.
- Code release 4 — Retention cron, CSV/JSON export, full-text body search (opsiyonel).
- Phase opsiyonel — call site enrichment (customerId/adminId ekleme).
Backfill yok — bugünden ileri log tutulur.
13. Open Decisions / Future Work
Bölüm başlığı “13. Open Decisions / Future Work”- Payload short-retention (30 gün) ayrı cron — şu an scope dışı, schema destekliyor.
- Key rotation —
key_versionkolonu eklenebilir; YAGNI, tek key. - Full-text body search — Phase 5’te ihtiyaca göre GIN index.
- TrialCredential plaintext encryption — ayrı security spec, security debt listesinde.
- AdminUser role/permission — şu an scope dışı, gelecekte super-admin ayrımı gerekirse.