Przykłady zakładają App Router, TypeScript i istniejącą warstwę sesji oraz bazy danych. Importy @/auth i @/lib/db reprezentują Twoją aplikację. Schematy tabel, uprawnienia organizacji i politykę storage musisz dopasować do jej modelu danych.
Upload plików do storage: usługa zarządzana czy własny bucket?
| Podejście | Kiedy ma sens | Co pozostaje po Twojej stronie |
|---|---|---|
| Usługa zarządzana, np. UploadThing | Szybkie wdrożenie uploadu i integracji z React | Uprawnienia, prywatność, weryfikacja zawartości, retencja i koszty |
| Własny S3 lub R2 | Własne reguły przechowywania i przetwarzania | Dodatkowo IAM, podpisy, CORS, obsługa zdarzeń i monitoring |
Odebranie pliku nie oznacza dopuszczenia go do obiegu. W UploadThing callback onUploadComplete powinien uruchamiać proces weryfikacji i zapisywać identyfikator oczekującego pliku. Nie przypisuj w nim od razu publicznego URL-a jako avatara, jeśli zawartość nie została sprawdzona. Wybierz prywatny dostęp zgodnie z dokumentacją UploadThing, a publiczny obraz twórz dopiero po kontroli i ponownym kodowaniu.
Dla obu wariantów ogranicz liczbę uploadów i łączną przestrzeń użytkownika. Sam limit rozmiaru jednego obiektu nie chroni przed wysłaniem tysięcy plików. Sposób ograniczania żądań opisuję w poradniku o rate limitingu w Next.js.
Kwarantanna, weryfikacja i udostępnienie
Proces podziel na stany pending, processing, ready oraz rejected. To kontrakt dla implementacji, obejmujący endpoint podpisujący, worker i endpoint pobierania.
- Serwer tworzy rekord uploadu. Po sprawdzeniu sesji, uprawnień i limitów zapisuje właściciela, losowy klucz kwarantanny, dozwolony format i termin ważności. Klucz pochodzi z serwera. Oryginalna nazwa jest wyłącznie metadanymi.
- Przeglądarka wysyła plik do prywatnej kwarantanny. Polityka storage zabrania publicznego odczytu, a podpis pozwala zapisać tylko wskazany obiekt. Endpointy aplikacji nie wydają adresów pobrania dla stanu
pending. - Worker wiąże kontrolę z konkretną zawartością. Sprawdza rekord uploadu i pobiera konkretną wersję obiektu albo tworzy prywatną, niezmienną kopię do kontroli. Waliduje rzeczywisty rozmiar, format i limity parsera. Zdarzenia storage mogą się powtarzać, więc przejęcie zadania musi być atomowe, a obsługa idempotentna.
- Poprawny wynik trafia pod nowy klucz w obszarze docelowym, do którego klient nie ma prawa zapisu. Worker publikuje dokładnie sprawdzone bajty lub wynik bezpiecznego przetworzenia, a następnie zapisuje
readyi klucz wyniku. Nie kopiuje ponownie bieżącego obiektu z kwarantanny, który mógł się zmienić podczas kontroli. - Błąd kontroli oznacza
rejected; błąd infrastruktury pozostawia plik niedostępny do czasu ponowienia. Reguły retencji usuwają odrzucone i porzucone uploady oraz osierocone wyniki. Pobieranie prywatnego pliku wymaga sprawdzenia właściciela i stanureadyprzed wydaniem krótkiego signed URL-a.
Presigned URL w S3 można używać wielokrotnie do wygaśnięcia, a zapis pod istniejącym kluczem może zastąpić obiekt. Losowa nazwa nie eliminuje tego ryzyka. Rozdzielenie obiektu przyjmującego upload od zweryfikowanego wyniku zapobiega podmianie udostępnionej zawartości starym podpisem.
Presigned POST w S3 i limit rozmiaru
W S3 formularzowy POST pozwala wymusić zakres rozmiaru w podpisanej polityce. Poniższy helper wywołuj wyłącznie po autoryzacji i utworzeniu rekordu pending; bucket kwarantanny musi mieć zablokowany publiczny dostęp. Dane uwierzytelniające pozostają na serwerze i mają uprawnienia ograniczone do potrzebnego prefiksu.
Przeglądarka wysyła FormData do zwróconego url, dodając wszystkie fields, a plik jako ostatnie pole. Zgodność pola Content-Type z podpisem nadal nie dowodzi zgodności zawartości. Endpoint wydający podpis musi walidować żądanie w runtime oraz stosować ochronę przed CSRF, jeśli korzysta z sesji cookie. CORS storage ogranicz do potrzebnych originów i metod; nie zastępuje on autoryzacji endpointu.
R2 nie obsługuje formularzowego presigned POST. Jego presigned URLs obsługują m.in. PUT. Nie przenoś powyższego kodu do R2 bez zmian. Jeśli potrzebujesz twardego limitu wejściowego, wybierz mechanizm, który rzeczywiście go egzekwuje. Kontrola rozmiaru po uploadzie i usunięcie obiektu chronią dalsze przetwarzanie, lecz nie cofają kosztów przyjęcia danych.
Co sprawdzać w zawartości pliku
file.type, rozszerzenie i rozmiar przesłany w JSON są deklaracjami klienta. Biblioteka file-type rozpoznaje sygnatury formatów binarnych, ale wynik nie potwierdza poprawności dokumentu ani braku szkodliwej zawartości. CSV nie ma jednoznacznych magic bytes, więc wymaga parsowania tekstu i walidacji rekordów.
Dla obrazu sprawdź liczbę bajtów, dozwolony format i wymiary, a następnie dekoduj go z limitem pikseli oraz czasu pracy. Przy avatarach zapisuj ponownie zakodowany wynik, usuwając zbędne metadane. Dla PDF-ów i innych dokumentów dobierz skanowanie antywirusowe lub rekonstrukcję zawartości do ryzyka aplikacji. Wynik skanera jest jedną z warstw ochrony.
Parsery uruchamiaj z ograniczeniem pamięci, czasu i współbieżności, szczególnie przy archiwach i skompresowanych formatach. Sam timeout obietnicy JavaScript nie zatrzymuje pracy synchronicznego parsera; izolowany worker lub proces musi dać się zakończyć. Zasady te rozwija OWASP File Upload Cheat Sheet.
Generowanie PDF z danych w Next.js
@react-pdf/renderer obsługuje przeglądarkę i serwer. Dla prywatnego raportu z bazy wybierz Node API i runtime = 'nodejs'. Pokazany wariant korzysta z lokalnego fontu oraz renderToBuffer; to uzasadnia wybór Node, bez odwoływania się do rzekomej zależności od natywnego canvas. Różnice środowisk opisuję w poradniku o Node i Edge Runtime.
Zarejestruj font zawierający polskie znaki. Dokumentacja react-pdf wymienia TTF i WOFF; dla pogrubienia dostarcz właściwy wariant. Poniżej prosty raport zamiast rozbudowanego szablonu faktury.
Dołącz plik fontu do artefaktu wdrożeniowego. Przy dynamicznych ścieżkach sprawdź wynik tracingu i w razie potrzeby skonfiguruj outputFileTracingIncludes w ustawieniach Next.js. Nie przyjmuj od użytkownika dowolnych adresów fontów ani obrazów do pobrania przez generator, ponieważ otwiera to drogę do SSRF.
Model report w przykładzie ma właściciela, tytuł i relację rows z polem text. Sprawdzenie ownerId chroni przed odczytem cudzego dokumentu po odgadnięciu ID. W aplikacji organizacyjnej zastosuj właściwe reguły dostępu zespołu.
Endpoint obejmij limitem żądań i równoległych renderów. renderToBuffer trzyma wynik w pamięci; większe raporty generuj w kolejce, zapisuj prywatnie i wydawaj po autoryzacji. Rejestruj awarie renderowania bez zapisywania treści dokumentu w logach. Cache gotowych plików musi uwzględniać właściciela i wersję danych.
Import CSV z podglądem przed zatwierdzeniem
Dla małych plików parsowanie w przeglądarce daje szybki podgląd. Poniższy komponent ogranicza plik do 50 KiB i 100 rekordów. Przycisk zatwierdzenia wywołuje onImport, które powinno zwrócić obietnicę odrzucaną przy błędzie serwera. Walidacja biznesowa odbywa się ponownie w Server Action.
Numer generation zapobiega nadpisaniu podglądu wynikiem starszego odczytu. Kolumny pochodzą z meta.fields; komponent odrzuca plik bez rekordów i nie uruchamia importu podczas samego parsowania.
Starsze eksporty CSV mogą używać kodowania systemowego, np. Windows-1250. Zależy to od wersji aplikacji i wybranego formatu zapisu. Zaproponuj eksport „CSV UTF-8” albo pozwól wskazać kodowanie przez prop encoding. Separator i kodowanie to odrębne ustawienia.
Walidacja i zapis w Server Action
Waliduj całe wejście przed pierwszym zapisem. Poniższy wariant przyjmuje ceny jako tekst z kropką i maksymalnie dwoma miejscami dziesiętnymi, a do bazy przekazuje całkowitą liczbę groszy. Odrzuca cały mały import, jeśli choć jeden rekord jest błędny.
Przykład zakłada transakcje w bazie i pole priceCents typu integer. Adapter onImport musi sprawdzić result.ok i odrzucić obietnicę przy błędzie, aby interfejs zachował dane. Własność pochodzi z sesji; użytkownik nie przesyła ownerId.
Transakcja zapobiega częściowemu zapisowi tej paczki, ale nie zapewnia idempotencji ponowionego żądania. W produkcji dodaj identyfikator importu lub paczki, przypisany do użytkownika, i unikalność tej pary w bazie. W jednej transakcji zapisz produkty oraz wynik paczki. Ponowienie z tym samym ID i tą samą zawartością zwraca zapisany wynik; inne dane pod tym ID odrzucasz. To zabezpiecza również sytuację, w której zapis się udał, lecz odpowiedź nie dotarła.
Duże importy i limity platformy
Server Actions mają domyślny limit body 1 MB, konfigurowalny przez experimental.serverActions.bodySizeLimit. Rozmiar CSV nie jest rozmiarem zserializowanego żądania. Vercel Functions ograniczają payload żądania i odpowiedzi do 4,5 MB; podniesienie limitu Next.js nie usuwa limitów hostingu.
Dla dużych importów użyj step lub chunk w Papa Parse. Ogranicz wielkość rekordu, paczki w bajtach, liczbę zapamiętanych błędów i równoległych wysyłek. Gdy wysyłasz paczkę, wstrzymaj parser i wznów po odpowiedzi, jeśli wybrany tryb to obsługuje. Wariant z workerem ma inne ograniczenia sterowania; samo worker: true nie kontroluje wzrostu bufora.
Długie zadania przenieś do kolejki z postępem, anulowaniem i idempotentnym zapisem paczek. Autoryzację oraz walidację testuj tak samo jak inne mutacje, zgodnie z poradnikiem o testach Server Actions i Route Handlers.
Eksport danych do CSV z Next.js
Escapowanie separatorów i cudzysłowów utrzymuje strukturę CSV. Osobno trzeba ograniczyć interpretację danych jako formuł. Dotyczy to również nagłówków, jeśli powstają z danych użytkownika. Poniższy wariant eksportuje komórki jako tekst przeznaczony do otwierania w arkuszu i obejmuje ochroną znaki sterujące oraz pełnoszerokie odpowiedniki prefiksów formuł.
Te ograniczenia opisuje OWASP CSV Injection. Dla integracji między systemami uzgodnij osobny format i zasady interpretacji wartości, zamiast bezwarunkowo zmieniać liczby ujemne w tekst.
BOM pomaga Excelowi rozpoznać UTF-8, lecz nie ustawia separatora. Przy ustawieniach regionalnych oczekujących średnika zaimportuj plik z jawnym wyborem przecinka albo przygotuj wariant eksportu z uzgodnionym separatorem. Jeśli potrzebujesz XLSX, instaluj SheetJS według instrukcji producenta; publiczny pakiet xlsx w npm jest nieaktualny.
Podgląd obrazów przed uploadem
Podgląd jest lokalny i nie potwierdza bezpieczeństwa obrazu. Krótki komponent dla pojedynczego avatara wystarczy, by pokazać zarządzanie pamięcią bez rozbudowanego drag-and-drop.
Cleanup efektu zwalnia URL przy zmianie pliku i odmontowaniu komponentu. unoptimized jawnie zaznacza lokalny charakter podglądu; Next.js automatycznie wyłącza optymalizację dla blob:. Serwer nadal musi przeprowadzić kontrolę obrazu, w tym limit pikseli po dekompresji. Dalsze przetwarzanie opisuję w poradniku o optymalizacji obrazów.
Co sprawdzić przed wdrożeniem
| Próba | Oczekiwany wynik |
|---|---|
| Cudze ID raportu lub uploadu | Brak dostępu do zawartości i podpisu pobierania |
| Plik większy niż limit, fałszywy MIME, uszkodzony obraz | Odrzucenie na właściwej warstwie, bez udostępnienia |
| Powtórny zapis starym podpisem po weryfikacji | Brak możliwości zmiany opublikowanego wyniku |
| Powtórzony webhook albo awaria skanera | Brak podwójnej publikacji, plik pozostaje niedostępny przy błędzie |
| Pusty CSV, same nagłówki, zmiana pliku w trakcie odczytu | Czytelny błąd lub aktualny podgląd, bez automatycznego importu |
| null, obiekt zamiast tablicy, zbyt długie pola w Server Action | Odrzucenie bez zapisu do bazy |
| Zerwane połączenie po zatwierdzonym zapisie | Ponowienie z tym samym ID nie duplikuje danych |
| Formuły, przecinki, cudzysłowy i nowe linie w nagłówkach oraz wartościach | Zachowana struktura i zweryfikowane zachowanie docelowego arkusza |
| Wiele równoległych uploadów lub renderów PDF | Egzekwowane limity użytkownika, czasu i zasobów |


