English summary. REST API of a banking application, built as a university project for two courses: Web Applications, and Information Systems in Banking. NestJS 11 + Prisma 6 + PostgreSQL (Supabase) + Redis, TypeScript in strict mode, running on Bun.
Authentication is implemented from scratch instead of delegating to a provider: JWT access/refresh pair, Argon2id password hashing, TOTP two-factor, brute-force lockout, and AES-256-GCM for sensitive secrets at rest. The banking domain covers accounts, transfers with idempotency keys, cards, loans with scoring and an instalment schedule, deposits with interest, a KYC gate, an audit log, PDF statements and an admin area.
Testing: 83 unit tests over the financial logic and crypto, plus an 18-step end-to-end journey that runs against a disposable Postgres + Redis pair in Docker and walks the full path — registration, KYC gate rejecting a transfer, admin approval, top-up, transfer, idempotency replay, insufficient funds, and RBAC. Design notes are in
docs/ARCHITECTURE.mdanddocs/THREAT-MODEL.md.The rest of this document is in Polish.
REST API aplikacji bankowej — backend projektu zaliczeniowego (Aplikacje Webowe + Systemy Informatyczne w Bankowości).
Testy:
bun run test(unit, 83 testy) ·bun run test:cov(coverage) ·bun run test:e2e(18 testów, pełna ścieżka HTTP). Logika finansowa (scoring, harmonogram rat, odsetki, IBAN/PESEL/Luhn) i krypto (AES-GCM, Argon2, TOTP) pokryte testami jednostkowymi — szczegóły wdocs/ARCHITECTURE.mdidocs/THREAT-MODEL.md.
bun run test:e2e jedną komendą: podnosi izolowany Postgres+Redis (docker-compose.test.yml,
porty 5433/6380 — nie dotyka Supabase), migruje, seeduje, odpala Jest i zawsze sprząta kontenery.
Scenariusz test/journey.e2e-spec.ts przechodzi całą ścieżkę bankową przez prawdziwe HTTP API:
admin login → rejestracja klienta → login → KYC GATE blokuje przelew (403)
→ submit KYC → admin approve → konto ACTIVE → admin doładowuje konto
→ przelew wewnętrzny → weryfikacja sald → IDEMPOTENCJA → brak środków (400)
→ RBAC (klient ≠ admin, 403) → brak tokenu (401)
Gdy kontenery testowe już działają, sam Jest: bun run test:e2e:jest
(start/stop ręcznie: bun run test:e2e:db:up / test:e2e:db:down).
- Runtime / PM: Bun 1.3+
- Framework: NestJS 11 (TypeScript strict, decorators)
- ORM: Prisma 6 (z
Prisma.Decimaldla pieniędzy) - DB: PostgreSQL (managed przez Supabase) — connection pooling (PgBouncer)
- Cache / sesje: Redis 7 (ioredis)
- Auth: własny — JWT access/refresh + Argon2id + 2FA TOTP
- Walidacja: class-validator + class-transformer (DTO) + zod (env)
- Docs: OpenAPI/Swagger pod
/docs(dev only) - Container: Docker (multi-stage,
oven/bun:1.3-alpine)
- Bun ≥ 1.3
- Docker + Docker Compose (na Redisa lokalnego)
- Projekt Supabase z
DATABASE_URL(pooler:6543) iDIRECT_URL(direct:5432)
# 1. Zależności
bun install
# 2. Środowisko
cp .env.example .env
# Uzupełnij wszystkie sekrety:
# openssl rand -base64 64 → JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, TWO_FACTOR_TOKEN_SECRET
# openssl rand -base64 32 → ENCRYPTION_KEY
# 3. Migracja Supabase + Prisma Client
bun run prisma:migrate:dev --name init
# 4. Seed: produkty lokat + pierwszy admin
# (wymaga ADMIN_EMAIL + ADMIN_PASSWORD w .env)
bun run db:seed
# 5. Redis lokalnie
docker compose up redis -d
# 6. Dev server
bun run start:devAPI: http://localhost:3001/api/v1. Swagger: http://localhost:3001/docs.
| Komenda | Opis |
|---|---|
bun run start:dev |
Hot-reload dev server |
bun run start:prod |
Production server (po build) |
bun run build |
Kompilacja do dist/ |
bun run lint |
ESLint --fix |
bun run typecheck |
tsc --noEmit |
bun run test |
Unit testy (jest) |
bun run test:e2e |
E2E testy |
bun run prisma:migrate:dev |
Migracja w trybie dev |
bun run prisma:migrate:deploy |
Migracje na produkcji (z .prisma/migrations/) |
bun run prisma:studio |
Prisma Studio (GUI do bazy) |
bun run db:seed |
Seed produktów lokat + admina |
docker compose up --buildPostgres NIE jest w docker-compose.yml — używamy zewnętrznego Supabase. W compose stoi tylko Redis + API.
src/
├── accounts/ # rachunki (CRUD, IBAN PL gen+validate, status)
├── admin/ # AdminController — lista użytkowników, suspend/activate/promote, stats
├── audit/ # AuditService — best-effort logger
├── auth/ # JWT + Argon2 + 2FA TOTP + brute-force lockout
├── cards/ # karty DEBIT (Luhn + AES-GCM + Argon2 PIN + reveal)
├── common/
│ ├── filters/ # global exception filter z mapping Prisma errors
│ ├── services/ # CryptoService (AES-256-GCM + SHA-256 + random)
│ └── utils/ # iban, pesel, card-number, loan-schedule, loan-scoring, deposit-interest
├── config/ # walidacja env (zod)
├── deposits/ # produkty lokat + Deposit lifecycle (open/close/break)
├── kyc/ # KycSubmission z review queue
├── loans/ # LoanApplication (scoring) + Loan + Installment (annuity/decreasing)
├── prisma/ # PrismaService (singleton)
├── redis/ # RedisService + module (globalny)
├── transfers/ # internal/external transfers, Serializable isolation
├── users/ # minimalny UsersService (lookup + toSafe)
├── app.module.ts
└── main.ts # bootstrap: helmet + cors + Swagger + global filter/pipe
prisma/
├── schema.prisma
└── seed.ts # produkty lokat + admin (idempotentny)
POST /register,POST /login,POST /login/2fa,POST /refresh,POST /logoutGET /me— profil zalogowanegoPOST /2fa/setup,POST /2fa/confirm,POST /2fa/disable
- Klient:
POST /submit,GET /me - Admin:
GET /pending,GET /:id,POST /:id/approve,POST /:id/reject
GET /,GET /:id,POST /(SAVINGS),PATCH /:id,DELETE /:id
POST /(auto internal/external po IBAN, idempotency)GET /(paginacja + filtry: accountId, type, status, dateRange)GET /:id
POST /(wydanie + zwrot plain PAN/CVV jednorazowo),GET /,GET /:idPOST /:id/block,POST /:id/unblockPATCH /:id/limits,POST /:id/change-pinPOST /:id/reveal-details(wymaga hasła)DELETE /:id(zamknięcie)
- Klient:
POST /applications,GET /applications,GET /applications/:id,DELETE /applications/:id GET /,GET /:id,POST /:id/installments/:installmentId/pay- Admin:
GET /admin/applications/pending,GET /admin/applications/:id,POST /admin/applications/:id/approve|reject|disburse
GET /products,GET /products/:idGET /,GET /:id,POST /,POST /:id/close,POST /:id/break- Admin:
GET /admin/products,POST /admin/products,POST /admin/products/:id/retire
GET /statsGET /users,GET /users/:idPOST /users/:id/suspend|activate|promote
- Decimal arithmetic: wszystkie kwoty to
Prisma.Decimal(19, 4). NIGDY Float / Number do operacji na pieniądzach. - Serializable isolation dla każdej operacji modyfikującej saldo (transfer, disbursement, loan repayment, deposit open/close/break).
- Idempotency: każda mutacja finansowa wymaga
idempotencyKey(UUID v4) —Transaction.idempotencyKeyma unique constraint. - KYC gate:
User.statusmusi byćACTIVEdla przelewów, kart, kredytów, lokat. Przed approve KYC klient jestPENDING_VERIFICATION. - Audit log: każda akcja wrażliwa zapisuje wpis przez
AuditService.log()(best-effort, nie blokuje requestu). - PESEL: surowy PESEL NIGDY nie trafia do bazy — tylko
sha256hash + ostatnie 4 cyfry. - PAN kart i CVV: szyfrowane AES-256-GCM. Reveal wymaga ponownej autoryzacji hasłem klienta.
- Helmet + CORS allowlist
- Rate limiting (Throttler) + dodatkowe limity per endpoint krytyczny
- Walidacja env przez zod (fail-fast przy starcie)
- Whitelist + forbidNonWhitelisted w ValidationPipe
- Argon2id dla haseł + PIN-ów kart
- AES-256-GCM dla 2FA seed, PAN kart, CVV
- JWT access (15min) + refresh (7d) z rotacją + replay detection
- 2FA TOTP (otplib + QR przez qrcode)
- Brute-force lockout (failedLoginCount + lockedUntil w User)
- Audit log dla wszystkich akcji wrażliwych
- Decimal(19,4) dla wszystkich kwot
- Idempotency keys dla wszystkich mutacji finansowych
- Serializable isolation dla operacji finansowych
- CI: lint + typecheck + build + Docker + CodeQL
- Penetration test — TODO przed produkcją