|
| 1 | +# API Endpoint Implementation Plan: POST /v1/send |
| 2 | + |
| 3 | +## 1. Przegląd punktu końcowego |
| 4 | + |
| 5 | +Punkt końcowy `POST /v1/send` jest kluczowym elementem systemu Requil, odpowiedzialnym za procesowanie wysyłki wiadomości e-mail. Realizuje on proces walidacji requestu, weryfikacji idempotencji, sprawdzania limitów (Rate Limit), renderowania treści na podstawie szablonu oraz finalnej wysyłki. |
| 6 | + |
| 7 | +Obsługuje dwa główne scenariusze użycia: |
| 8 | +1. **Wysyłka API (Server-to-Server)**: Autoryzacja przez API Key, używana przez integracje klientów. |
| 9 | +2. **Wysyłka Testowa (Dashboard)**: Autoryzacja przez sesję użytkownika (Supabase Auth), używana do testowania szablonów bezpośrednio z panelu administracyjnego. |
| 10 | + |
| 11 | +## 2. Szczegóły żądania |
| 12 | + |
| 13 | +- **Metoda HTTP**: `POST` |
| 14 | +- **Struktura URL**: `/v1/send` |
| 15 | +- **Autoryzacja**: |
| 16 | + - `Bearer {api_key}` (API Key ze scope `send`). |
| 17 | + - LUB Cookie Sesyjne (dla zapytań z Dashboardu - wysyłka testowa). |
| 18 | +- **Nagłówki**: |
| 19 | + - `Idempotency-Key` (Wymagany dla API Key): Unikalny klucz requestu. Opcjonalny dla wysyłki testowej z Dashboardu. |
| 20 | + - `Authorization`: `Bearer {api_key}` (opcjonalne, jeśli sesja). |
| 21 | + |
| 22 | +- **Parametry Body (JSON)**: |
| 23 | + |
| 24 | +| Pole | Typ | Wymagalność | Opis | |
| 25 | +|------|-----|-------------|------| |
| 26 | +| `template` | string | **Wymagane** | Alias lub ID szablonu do użycia. | |
| 27 | +| `to` | array | **Wymagane** | Lista odbiorców (min. 1). | |
| 28 | +| `to[].email` | string | **Wymagane** | Adres email odbiorcy. | |
| 29 | +| `to[].variables` | object | Opcjonalne | Zmienne do interpolacji dla konkretnego odbiorcy. | |
| 30 | +| `transport` | string | Opcjonalne | `resend` \| `smtp`. Domyślnie używa konfiguracji workspace lub fallback do internal. | |
| 31 | +| `subject` | string | Opcjonalne | Nadpisuje temat zdefiniowany w szablonie. | |
| 32 | +| `preheader` | string | Opcjonalne | Nadpisuje preheader z szablonu. | |
| 33 | + |
| 34 | +## 3. Wykorzystywane typy |
| 35 | + |
| 36 | +### DTO (Data Transfer Objects) |
| 37 | + |
| 38 | +Zdefiniowane za pomocą Zod Schema w `modules/sending/sending.schema.ts` (do utworzenia): |
| 39 | + |
| 40 | +```typescript |
| 41 | +// Uproszczony schemat |
| 42 | +const SendEmailSchema = z.object({ |
| 43 | + template: z.string(), |
| 44 | + transport: z.enum(['resend', 'smtp']).optional(), |
| 45 | + subject: z.string().optional(), |
| 46 | + preheader: z.string().optional(), |
| 47 | + to: z.array(z.object({ |
| 48 | + email: z.string().email(), |
| 49 | + variables: z.record(z.any()).optional() |
| 50 | + })).min(1).max(500) // Limit batcha |
| 51 | +}); |
| 52 | +``` |
| 53 | + |
| 54 | +### Command Models |
| 55 | + |
| 56 | +Command w architekturze CQRS (`modules/sending/commands/send-email/send-email.command.ts`): |
| 57 | + |
| 58 | +```typescript |
| 59 | +export class SendEmailCommand { |
| 60 | + constructor( |
| 61 | + public readonly workspaceId: string, |
| 62 | + public readonly idempotencyKey: string, |
| 63 | + public readonly payload: SendEmailDto, |
| 64 | + public readonly traceId: string |
| 65 | + ) {} |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +## 4. Szczegóły odpowiedzi |
| 70 | + |
| 71 | +**Sukces (200 OK)** - Zwracany, gdy job został przyjęty i przetworzony (lub zakolejkowany). |
| 72 | + |
| 73 | +```json |
| 74 | +{ |
| 75 | + "ok": true, |
| 76 | + "job_id": "job_123...", |
| 77 | + "used_template_snapshot_id": "snap_456...", |
| 78 | + "sent": 1, |
| 79 | + "failed": 0, |
| 80 | + "warnings": [] |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +**Błędy**: |
| 85 | +- `400 Bad Request`: Błąd walidacji danych (np. brak zmiennej). |
| 86 | +- `401 Unauthorized`: Nieprawidłowy API Key. |
| 87 | +- `409 Conflict`: Konflikt klucza idempotencji (użyty wcześniej z innym body) lub brak opublikowanego snapshotu. |
| 88 | +- `429 Too Many Requests`: Przekroczony limit zapytań dla workspace. |
| 89 | +- `500 Internal Server Error`: Błąd wewnętrzny. |
| 90 | + |
| 91 | +## 5. Przepływ danych |
| 92 | + |
| 93 | +1. **Request**: Klient (API lub Dashboard) wysyła `POST /v1/send`. |
| 94 | +2. **Auth Guard**: |
| 95 | + - **API Key**: Weryfikacja klucza, scope `send`, pobranie `workspace_id`. |
| 96 | + - **Session**: Weryfikacja sesji użytkownika (Supabase), sprawdzenie członkostwa w workspace (Header `x-workspace-id` lub context). |
| 97 | +3. **Idempotency Check (Redis)**: |
| 98 | + - Dla API Key: Wymagane sprawdzenie klucza `lock:send:{key}`. |
| 99 | + - Dla Dashboardu: Opcjonalne (można pominąć dla pojedynczych testów). |
| 100 | +4. **Rate Limit Check (Upstash)**: Weryfikacja limitu tokenów dla workspace. |
| 101 | +5. **Handler (CQRS)**: |
| 102 | + - Pobierz najnowszy snapshot szablonu (`template_snapshots`). |
| 103 | + - Walidacja zmiennych odbiorców względem schema snapshotu (AJV). |
| 104 | + - Utworzenie rekordu `SendJob` (status `pending`) oraz `SendRecipients` w DB. |
| 105 | + - **Renderowanie**: Wykorzystanie silnika **React Email**. |
| 106 | + - JSON Document (z DB) -> React Components -> HTML. |
| 107 | + - Interpolacja zmiennych. |
| 108 | + - Inicjalizacja transportu (Internal Resend lub Custom z DB). |
| 109 | + - Wysyłka via `@requil/transports`. |
| 110 | + - Aktualizacja statusu `SendJob` i `SendRecipients` (sent/failed). |
| 111 | +6. **Response**: Zwrócenie wyniku i zapisanie go w cache idempotencji. |
| 112 | + |
| 113 | +## 6. Względy bezpieczeństwa |
| 114 | + |
| 115 | +- **Dual Auth**: Obsługa zarówno API Key (S2S) jak i Sesji (Dashboard). Dashboard wymaga poprawnej konfiguracji CORS (whitelist domeny dashboardu) lub proxy. |
| 116 | +- **API Key Scopes**: Wymagany scope `send` dla kluczy API. |
| 117 | +- **Workspace Isolation**: `workspace_id` zawsze weryfikowany (z klucza lub uprawnień użytkownika). |
| 118 | +- **Input Validation**: Strict schema validation (Zod). |
| 119 | +- **HTML Sanitization**: React Email zapewnia escaping zmiennych, chroniąc przed XSS. |
| 120 | +- **CORS**: Endpoint musi być dostępny dla Dashboardu. Należy zweryfikować konfigurację w `apps/api/src/server/plugins/cors.ts` lub zezwolić na specyficzne originy dla tego route'a. |
| 121 | + |
| 122 | +## 7. Obsługa błędów |
| 123 | + |
| 124 | +Błędy są mapowane na odpowiednie kody HTTP przez globalny `ErrorHandler`: |
| 125 | + |
| 126 | +- `TemplateNotFoundError` -> 404 |
| 127 | +- `NoPublishedSnapshotError` -> 409 |
| 128 | +- `IdempotencyConflictError` -> 409 |
| 129 | +- `RateLimitExceededError` -> 429 |
| 130 | +- `ValidationError` -> 400 (szczegóły w body) |
| 131 | +- `TransportError` -> 502/503 (jeśli retry nie pomogło) |
| 132 | + |
| 133 | +## 8. Wydajność |
| 134 | + |
| 135 | +- **Redis Cache**: Wyniki idempotencji i snapshoty szablonów cache'owane w Redis. |
| 136 | +- **Batch Insert**: `SendRecipients` zapisywane przy użyciu `db.insert().values([...])` dla wydajności. |
| 137 | +- **Connection Pooling**: Supabase pooler obsługuje połączenia DB. |
| 138 | +- **Future Optimization**: Dla dużych batchy (>500), handler powinien tylko kolejkować job (QStash) i zwracać 202 Accepted (post-MVP). |
| 139 | + |
| 140 | +## 9. Etapy wdrożenia |
| 141 | + |
| 142 | +### Krok 1: Scaffolding Modułu |
| 143 | +Utworzenie struktury katalogów w `apps/api/src/modules/sending`: |
| 144 | +- `sending.schema.ts` (Zod schema) |
| 145 | +- `commands/send-email/` (Route, Handler, Command) |
| 146 | +- `database/` (Repository port i implementacja Drizzle) |
| 147 | +- `domain/` (Model domenowy, błędy) |
| 148 | + |
| 149 | +### Krok 2: Implementacja Warstwy Danych |
| 150 | +Implementacja `SendingRepository`: |
| 151 | +- Metoda `createJob(job: NewSendJob, recipients: NewSendRecipient[])`: Transakcyjny zapis joba i odbiorców. |
| 152 | +- Metoda `updateJobStatus(...)`. |
| 153 | +- Metoda `updateRecipientStatus(...)`. |
| 154 | + |
| 155 | +### Krok 3: Implementacja Logiki Domenowej (Handler) |
| 156 | +Implementacja `SendEmailHandler`: |
| 157 | +- Integracja z `TemplateRepository` (pobranie snapshotu). |
| 158 | +- Logika walidacji zmiennych. |
| 159 | +- Logika renderowania (wykorzystanie **React Email** engine/komponentów). |
| 160 | +- Integracja z `@requil/transports`. |
| 161 | + |
| 162 | +### Krok 4: Endpoint i Middleware |
| 163 | +Konfiguracja `send-email.route.ts`: |
| 164 | +- Rejestracja w Fastify. |
| 165 | +- Podpięcie walidacji Zod. |
| 166 | +- Konfiguracja **Auth Pre-handler**: |
| 167 | + - Sprawdź API Key. |
| 168 | + - Jeśli brak, sprawdź sesję (Supabase Auth). |
| 169 | + - Jeśli oba brak -> 401. |
| 170 | +- Obsługa nagłówka `Idempotency-Key` (warunkowa dla API Key). |
| 171 | +- Weryfikacja ustawień CORS dla endpointu (upewnienie się, że Dashboard ma dostęp). |
| 172 | + |
| 173 | +### Krok 5: Testy |
| 174 | +- Unit testy dla Handlera (mockowanie repozytoriów, React Email i transportu). |
| 175 | +- Integration testy z bazą danych (Testcontainers). |
| 176 | +- Scenariusze testowe: |
| 177 | + - Wysyłka z poprawnym API Key. |
| 178 | + - Wysyłka testowa z sesji użytkownika. |
| 179 | + - Błąd walidacji. |
| 180 | + - Konflikt idempotencji. |
| 181 | + |
0 commit comments