327d6989454bfa75ad0dda996488eb6eaf2f5a72
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lands the §9.3 "compliance + brand" tier of mailAudit.md as one PR. Six changes share enough surface (notifications, shared utils, infrastructure) that splitting them would require multiple stacked PRs. #9 — Turkish-locale title-case for names at signup • New `normalizeName()` in @sase/shared, locale-aware (İ/ı pairs handled via toLocaleLowerCase('tr-TR') + matching toLocaleUpperCase). Hyphen- aware, collapses whitespace, idempotent. • Wired into better-auth's `user.create.before` hook so every new signup gets canonicalised before the row lands in Postgres. • 28 unit tests in packages/shared/src/index.spec.ts. • Backfill script at `scripts/backfill-user-names.ts` (already run against prod + dev — 210/402 prod users and 72/153 dev users canonicalised, plus 71 Novu subscribers). #10 — Email typo correction at signup • New `suggestEmailFix()` in @sase/shared: exact-match typo dictionary for the addresses we've actually suppressed (icould.com, gmial.com, xn--gmail-bgd.com, …) plus Levenshtein ≤ 2 fallback against popular providers. • Inline UI hint on the register form — "Bunu mu demek istedin? <link>" that swaps the email on click; PostHog event tracks acceptance. #11 — Strip EN branches (decision: TR-only) • 0/205 prod subscribers have locale='en' and there's no marketing in English — the {{#equals subscriber.locale "en"}}…{{else}}…{{/equals}} framework was dead code in all 10 templates. • Templates updated in-place (avg ~30 % smaller). Renamed `novu-welcome-tr.html` → `novu-welcome.html` for consistency with the other 9 files. • Novu workflow definitions in both Dev + Prod envs updated via Mongo: subjects collapsed to TR-only, content replaced with new HTML (mongodump/restore-safe). • App code: `NovuRecipient.locale` and `NovuUser.locale` removed; the `...(user.locale === "en" ? { locale: "en" } : {})` spread in NovuService is gone. #12 — DKIM rotated to 2048-bit RSA • Postal default was 1024-bit (selector `postal-YeIm3w`). Generated new 2048-bit key, added DNS TXT `postal-2k260604._domainkey.sase.tr`, atomically swapped `domains.dkim_identifier_string` + `dkim_private_key` in Postal MariaDB, restarted Postal SMTP. • Verified: outgoing welcome mail now signs with `s=postal-2k260604` and a 256-byte signature body (vs the previous 128-byte 1024-bit signature). Pubkey on DNS matches the private key. • OLD TXT record (`postal-YeIm3w._domainkey`) stays in DNS for ~7 days as a grace window for in-flight mail. #13 — MTA-STS + TLS-RPT • Extended the existing mailtrack Cloudflare Worker to also serve `mta-sts.sase.tr/.well-known/mta-sts.txt` (`mode: enforce, mx: mx.postal.sase.tr, max_age: 604800`). Workers Domain bound to the mailtrack service via Cloudflare API. • DNS: `_mta-sts.sase.tr` TXT "v=STSv1; id=20260604111347" `_smtp._tls.sase.tr` TXT "v=TLSRPTv1; rua=mailto:dmarc@sase.tr" • Verified policy fetch returns 200 with the expected body; cert valid (sase.tr SAN issued by GTS). #14 — Unsubscribe preferences + RFC 8058 one-click endpoint • New `email_preferences` table (migration 0011) keyed (user_id, workflow), captures source for audit (one_click / manual_link / settings_page). • New `UnsubscribeController` at `/api/email/unsubscribe`: - POST: Gmail/Yahoo one-click bot path (200 fast) - GET: human-visit, renders a Turkish confirmation page Both validate an HMAC-SHA256(`userId|workflow`) token under `UNSUBSCRIBE_SECRET` — stateless, no DB lookup to validate, secret rotation invalidates all outstanding tokens. • `triggerNovu()` now mints the per-call `overrides.email.headers`: `List-Unsubscribe: <https://…?u=&w=&t=>, <mailto:unsubscribe@…>` `List-Unsubscribe-Post: List-Unsubscribe=One-Click` Auth + payment workflows opt out via NO_UNSUBSCRIBE_WORKFLOWS so the unsubscribe URL never appears on transactional mail. • `NovuService.trigger()` pre-flight-checks `isOptedOut()` and skips the trigger entirely if the user opted out. Fail-open on DB error so a transient blip can't swallow auth mail. • `lifecycle-email.processor.ts` (standalone BullMQ worker — no NestJS DI) does the same check inline via a LEFT JOIN on `email_preferences WHERE opted_out IS NULL`. • Coolify env wired in both Prod and Dev apps: `UNSUBSCRIBE_SECRET` (32-byte hex, distinct per env) `UNSUBSCRIBE_URL_BASE` = `https://(dev.)sase.tr/api/email/unsubscribe` ## Companion sibling changes (already applied, NOT in this PR) - Cloudflare worker `mailtrack` redeployed with mta-sts.sase.tr custom domain. - Postal MariaDB `domains.dkim_identifier_string` + `dkim_private_key` updated to the new 2k260604 selector (live since 2026-06-04 11:18). - `postal-2k260604._domainkey.sase.tr` TXT record live at Cloudflare. - `_mta-sts.sase.tr` + `_smtp._tls.sase.tr` TXT records live at Cloudflare. - Novu Mongo notification + message templates updated to TR-only. - 282 user names canonicalised across prod + dev + Novu subscribers. ## Verification snapshot - Postal raw_headers (ID 157, post-rotation): `s=postal-2k260604` + 256-byte b= - `dig +short TXT _mta-sts.sase.tr @1.1.1.1` ⇒ live id=20260604111347 - `curl https://mta-sts.sase.tr/.well-known/mta-sts.txt` ⇒ 200 with policy - 28 unit tests (normalizeName + suggestEmailFix) all green via Node sanity. ## Deploy notes - Re-run `pnpm db:generate` to regenerate the drizzle snapshot for 0011 (added the journal entry manually because no drizzle-kit on this box). - Run `pnpm tsx scripts/backfill-user-names.ts --apply` against any DB not yet canonicalised (already done for prod + dev today). - The host-side Novu nodemailer-headers patch at `postal/novu-patches/apply-headers-patch.sh` must be re-run after every Novu container redeploy or the List-Unsubscribe header is silently dropped before reaching Postal (see audit §9.1 #3 for the upstream cause). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sase
VIN/şase numarası sorgulama ve otomotiv yedek parça katalog platformu (Türkiye). Site: https://sase.tr · Staging: https://dev.sase.tr
Stack
| Katman | Teknoloji |
|---|---|
| Monorepo | pnpm 10 + Turborepo |
| Backend | NestJS 10, TypeScript 5.7, Node 22 |
| DB | PostgreSQL 17 + Drizzle ORM |
| Cache / Queue | Redis 7 + BullMQ |
| Auth | Better Auth (cookie session) |
| Frontend | Vite 6, React 19, TanStack Router + Query |
| UI | Tailwind v4, shadcn/ui (Radix) |
| Ödeme | Stripe (kart) |
| Postal | |
| Storage | MinIO (S3) |
| Analitik | PostHog |
| Test | Vitest, Playwright |
| Deploy | GitHub Actions → SSH → PM2 |
Yapı
ss/
├── apps/
│ ├── api/ NestJS (port 4000, /api)
│ └── web/ Vite + React (port 3000)
├── packages/
│ ├── shared/ Tipler, Zod şemaları
│ ├── config/ Env şeması (Zod)
│ └── ui/ Paylaşılan shadcn bileşenleri
├── docker/ docker-compose + nginx
├── scripts/ Deploy + bakım scriptleri
└── docs/ Detay dökümantasyon (INDEX.md)
Komutlar
pnpm dev # tüm app'ler (Turbo)
pnpm build
pnpm test # Vitest
pnpm lint # Biome
pnpm typecheck
# DB (apps/api/)
pnpm db:push # şemayı push
pnpm db:studio # Drizzle Studio
pnpm db:generate # migration üret
# Tek app
pnpm dev --filter=api
pnpm dev --filter=web
VIN Decode Akışı
Corgi (offline WMI) → PartsCatalogs API → PL24 API → EMEX scraper → NHTSA
Birden fazla araç eşleşmesi gelirse frontend seçim modalı gösterir.
Katalog Mimarisi
- PL24 P5 Modern (REST/JSON) — VW Group, BMW, Mercedes, Renault, Toyota...
- PL24 P4 Legacy (HTML scrape) — Ford, PSA, Hyundai/Kia, Nissan, Opel, Volvo
- PartsCatalogs (REST + Playwright JWT) — geniş VIN kapsamı
- EMEX (Playwright scrape,
emexdwc.ae) — async BullMQ worker
EMEX & PartsCatalogs Türkçeleştirme
EMEX ve PartsCatalogs kaynakları kategori/parça isimlerini İngilizce (zaman zaman Rusça) döner. Çeviri akışı:
TranslationsService(apps/api/src/translations/) — Redis cache → DB lookup (emex_category_translations) → orijinal döndür. Dictionary tabanlı word-by-word replace devre dışı (yarı-İngilizce çıktı yaratıyordu).- Hot path (
categories.service.ts) — EMEX/PCAT insert noktalarındatranslateMany()ile bulk lookup yapılır; cache miss varsa orijinal yazılır, sonraki bootstrap pass'inde LLM ile çevrilir. - Bootstrap script (
scripts/emex-translate-bootstrap.ts) — DB'deki tüm uniquename_originaldeğerlerini OpenRouter üzerinden DeepSeek V3 ile çevirir,emex_category_translationstablosuna yazar. - Backfill script (
scripts/emex-backfill-tr-names.ts) — translation tablosuna göre mevcutcategories.nameveparts.namedeğerlerini günceller, Redis cache flush eder.
# 1) Tüm yeni terimleri çevir (~$0.50, ~30 dk)
pnpm exec tsx scripts/emex-translate-bootstrap.ts --dry-run
pnpm exec tsx scripts/emex-translate-bootstrap.ts
# 2) DB kayıtlarını güncelle
pnpm exec tsx scripts/emex-backfill-tr-names.ts --dry-run
pnpm exec tsx scripts/emex-backfill-tr-names.ts
# 3) API restart (yeni hot-path translateMany için)
pm2 restart sase-api
Env: OPENROUTER_API_KEY (apps/api/.env).
Test Bilgileri
- Admin:
admin@sase.tr/Sase2026 - Test VIN (VW):
WVWZZZ1JZ3W597935 - Login:
POST /api/auth/sign-in/email→ cookiebetter-auth.session_token
Daha Fazla
docs/INDEX.md— kapsamlı proje rehberi (rotalar, API, DB şeması, bileşenler)CLAUDE.md— Claude Code için proje rehberi
Description
Languages
TypeScript
74.8%
HTML
20.2%
JavaScript
4%
Shell
0.5%
PLpgSQL
0.2%
Other
0.2%