Files
whowhat/README.md
2026-07-10 22:57:16 +02:00

11 KiB
Raw Blame History

KtoCo — wspólne wydatki dla grup, par i współlokatorów

Aplikacja webowa (PWA) do dzielenia się wydatkami w gospodarstwie domowym — kto ile wydał, kto komu jest winien, z historią, statystykami i rozliczeniami. Obsługuje dowolną liczbę osób w gospodarstwie i dowolną liczbę gospodarstw na użytkownika. Mobile-first, instalowalna na telefonie, działa częściowo offline.

Funkcje

  • Dashboard — kafelki „kto komu ile jest winien" (automatycznie uproszczone do minimalnej liczby przelewów dla całej grupy) z przyciskiem „Rozlicz się", wykres kołowy wydatków wg kategorii, podsumowanie miesiąca.
  • Dodawanie wydatku — duże pole kwoty, kategorie z ikonami, wybór płacącego, trzy tryby podziału: po równo (między wszystkich członków), dokładny podział, całość na jedną osobę.
  • Historia — lista wydatków z filtrami (miesiąc/kategoria/płacący), edycja i usuwanie.
  • Statystyki — wydatki miesiąc do miesiąca, porównanie konsumpcji członków gospodarstwa, ranking kategorii.
  • Wiele gospodarstw — użytkownik może należeć do wielu gospodarstw jednocześnie i przełączać się między nimi (Ustawienia → „Twoje gospodarstwa"); każde gospodarstwo może mieć dowolną liczbę członków, dołączanych tym samym kodem zaproszenia.
  • Ustawienia — przełącznik i zarządzanie gospodarstwami (zmiana nazwy/waluty, usuwanie członka, usunięcie całego gospodarstwa, zaproszenia), własne kategorie (z edycją nazwy/ikony), motyw jasny/ciemny/systemowy, powiadomienia mailowe, eksport CSV.
  • Konto — rejestracja e-mail/hasło, logowanie, zmiana nazwy, zmiana hasła, przypomnienie/reset hasła mailem, usunięcie konta.
  • PWA / offline — instalowalna na ekranie głównym telefonu, cache widoków, wydatki dodane offline trafiają do kolejki i synchronizują się automatycznie po powrocie sieci.

Stack technologiczny

Warstwa Technologia
Frontend React (Vite), react-router-dom, TanStack Query, Recharts, vite-plugin-pwa, Material Symbols (Google Fonts)
Backend Node.js + Express, better-sqlite3, JWT (jsonwebtoken), bcryptjs, nodemailer
Baza danych SQLite (plik na bind mouncie ./sqlite)
Infrastruktura Docker Compose, nginx (serwuje frontend + proxy /api), opcjonalnie Traefik (TLS + routing domenowy)

Struktura projektu

ktoco/
├── docker-compose.yaml       # jedyny plik potrzebny do uruchomienia całości
├── .env                      # konfiguracja (sekrety, domena, SMTP) — NIE commitować
├── .env.example              # szablon konfiguracji do skopiowania
├── sqlite/                   # bind mount — tu leży plik app.db (trwałość danych)
├── backend/
│   ├── Dockerfile
│   └── src/
│       ├── index.js              # Express app, montowanie routerów
│       ├── db/                   # schema.sql + połączenie better-sqlite3
│       ├── middleware/auth.js     # weryfikacja JWT
│       ├── routes/                # auth, households, categories, expenses, settlements, stats
│       └── utils/                 # obliczanie salda, mailer (nodemailer), pomocnicze household
└── frontend/
    ├── Dockerfile                 # multi-stage: build (node) -> serve (nginx)
    ├── nginx.conf                 # proxy /api -> backend:3000
    ├── vite.config.js             # konfiguracja PWA (manifest, service worker)
    └── src/
        ├── pages/                 # Dashboard, AddExpense, History, Stats, Settings, Login, Register, ...
        ├── components/            # BottomNav, wykresy, formularze, Icon, Switch, ...
        ├── api/                   # klient fetch + hooki React Query
        ├── auth/                  # kontekst autoryzacji (JWT w localStorage)
        ├── household/             # kontekst aktywnego gospodarstwa (lista + przełączanie)
        ├── theme/                 # kontekst motywu jasny/ciemny/systemowy
        └── offline/                # kolejka IndexedDB + synchronizacja po powrocie sieci

Uruchomienie

Wymagany jest tylko Docker (z pluginem Compose).

cp .env.example .env
# uzupełnij .env (patrz sekcja niżej) — nie trzeba edytować docker-compose.yaml
sudo docker compose up -d --build

Aplikacja będzie dostępna pod http://localhost:8856 (oraz pod domeną z Traefika, jeśli skonfigurowana — patrz niżej).

Konfiguracja (.env)

Cała konfiguracja wdrożeniowa znajduje się w .envdocker-compose.yaml nie wymaga edycji.

Zmienna Opis Domyślnie
JWT_SECRET Sekret do podpisywania tokenów logowania. Wygeneruj: openssl rand -hex 32 — (wymagany)
FRONTEND_URL Publiczny adres aplikacji, używany w linkach w mailach (np. reset hasła) https://ktoco.kzbikowski.pl
DOMAIN Domena, pod którą Traefik wystawia aplikację ktoco.kzbikowski.pl
TRAEFIK_NETWORK Nazwa istniejącej zewnętrznej sieci Docker, do której podłączony jest Traefik traefik_public
SMTP_HOST Adres serwera SMTP. Puste = wysyłka maili wyłączona (tylko log w konsoli) — (opcjonalne)
SMTP_PORT Port SMTP 587
SMTP_SECURE true dla połączenia SSL/TLS od razu (port 465), inaczej false (STARTTLS) false
SMTP_USER / SMTP_PASS Dane logowania do SMTP
SMTP_FROM Adres nadawcy w wysyłanych mailach SMTP_USER

Bez skonfigurowanego SMTP aplikacja działa normalnie — funkcje „reset hasła" i „powiadomienia mailowe" po prostu nie wysyłają realnych maili (backend loguje w konsoli, że wysyłkę pominięto).

Dane / trwałość

Baza SQLite leży w ./sqlite/app.db na hoście (bind mount, nie nazwany wolumen Dockera) — łatwo ją skopiować, zbackupować albo podejrzeć narzędziem sqlite3 bez wchodzenia do kontenera.

Wdrożenie za Traefikiem

Serwis frontend jest podłączony do zewnętrznej sieci traefik_public (nazwa konfigurowalna przez TRAEFIK_NETWORK) i ma etykiety Traefika (routing po domenie z .env, TLS przez tls-resolver). Warunek: sieć traefik_public musi już istnieć na hoście (tworzy ją zwykle stack samego Traefika):

docker network create traefik_public   # tylko jeśli jeszcze nie istnieje

Port 8856 frontend jest dodatkowo opublikowany bezpośrednio na hosta — przydatne przy testach lokalnych równolegle z dostępem przez Traefik.

Model danych (SQLite)

  • users — konta (e-mail, hash hasła, preferencja powiadomień mailowych)
  • households — gospodarstwa domowe (nazwa, waluta)
  • household_members — członkowie gospodarstwa (dowolna liczba osób; użytkownik może być w wielu gospodarstwach naraz)
  • invites — kody zaproszeń do gospodarstwa (ważne 7 dni, wielokrotnego użytku — nie wygasają po jednym dołączeniu)
  • password_resets — jednorazowe tokeny resetu hasła (ważne 1h)
  • categories — kategorie wydatków (nazwa, ikona Material Symbols, kolor)
  • expenses — wydatki (kwota, płacący, kategoria, data, typ podziału)
  • expense_shares — finalny podział wydatku między członków gospodarstwa (niezależnie od typu podziału zawsze sumuje się do kwoty wydatku)
  • settlements — historia rozliczeń („Rozlicz się")

Saldo per osoba liczone jest jako: (suma zapłacona przez osobę) (suma jej udziałów w wydatkach) (netto rozliczeń). Do prezentacji „kto komu ile jest winien" salda są upraszczane zachłannym algorytmem (backend/src/utils/balance.js: simplifyDebts), który dla N osób generuje minimalną liczbę przelewów rozliczających wszystkich (zamiast osobnego długu między każdą parą).

Wiele gospodarstw — jak to działa

Użytkownik może należeć do wielu gospodarstw. Ponieważ każdy request do zasobów powiązanych z gospodarstwem (wydatki, kategorie, saldo, statystyki, rozliczenia) musi wiedzieć, którego gospodarstwa dotyczy, frontend wysyła nagłówek X-Household-Id: <id aktywnego gospodarstwa> przy każdym takim żądaniu (ustawiany automatycznie przez frontend/src/household/HouseholdContext.jsx po przełączeniu gospodarstwa w Ustawieniach). Backend weryfikuje w requireHousehold middleware, że zalogowany użytkownik faktycznie jest członkiem podanego gospodarstwa.

Usunięcie ostatniego członka z gospodarstwa automatycznie kasuje samo gospodarstwo (wraz z całą historią wydatków — kasowanie kaskadowe przez klucze obce). Usunięcie konta użytkownika, który ma współdzieloną historię finansową z innymi (wydatki/udziały/rozliczenia), nie usuwa go fizycznie z bazy (zepsułoby to historię widoczną dla reszty gospodarstwa) — konto jest wtedy anonimizowane (nazwa → „Usunięte konto", e-mail zastąpiony unikalnym nieistniejącym adresem, hasło unieważnione). Świeże konto bez żadnej historii jest usuwane w całości.

API (skrót)

Wszystkie endpointy poza /auth/register, /auth/login, /auth/forgot-password, /auth/reset-password i /health wymagają nagłówka Authorization: Bearer <token>. Endpointy gospodarstwa/kategorii/wydatków/rozliczeń/statystyk dodatkowo wymagają X-Household-Id: <id>.

Grupa Endpointy
Auth POST /auth/register, /login, /change-password, /forgot-password, /reset-password, GET /auth/me, PUT /auth/me, PUT /auth/me/notifications, DELETE /auth/me
Gospodarstwa GET /households (lista Twoich), GET/PUT/DELETE /households/:id, POST /households, POST /households/:id/invite, POST /households/join, DELETE /households/:id/members/:userId
Kategorie GET/POST/PUT/DELETE /categories[/:id]
Wydatki GET/POST/PUT/DELETE /expenses[/:id] (filtry: month, categoryId, payerId)
Rozliczenia GET/POST /settlements (POST rozlicza od razu wszystkie uproszczone przelewy)
Statystyki GET /stats/balance, /summary, /monthly, /export.csv

Tryb offline (PWA)

Service worker (Workbox, przez vite-plugin-pwa) cache'uje powłokę aplikacji i ostatnio pobrane dane GET z API (strategia NetworkFirst), więc appka otwiera się i pokazuje dane nawet bez sieci. Nowy wydatek dodany offline trafia do kolejki w IndexedDB (frontend/src/offline/) i zostaje automatycznie wysłany po wykryciu powrotu połączenia (online event) — widoczny jest wtedy baner z liczbą oczekujących wpisów.

Znane ograniczenia

  • Kopiowanie kodu zaproszenia przez navigator.clipboard wymaga bezpiecznego kontekstu (HTTPS lub localhost) — na zwykłym HTTP w sieci lokalnej przeglądarka może to zablokować; dlatego kod jest zawsze dostępny też jako zaznaczalne pole tekstowe (ręczne kopiowanie zawsze działa).
  • schema.sql używa CREATE TABLE IF NOT EXISTS — dodanie nowej kolumny do istniejącej tabeli w już działającej bazie wymaga ręcznej migracji (ALTER TABLE); przy starcie na czystej bazie schemat tworzy się poprawnie od razu.
  • Kod zaproszenia do gospodarstwa nie wygasa po pierwszym użyciu (celowo — pozwala zaprosić dowolną liczbę osób tym samym kodem), tylko po czasie (7 dni) lub ręcznym wygenerowaniu nowego w Ustawieniach.

Rozwój lokalny (bez Dockera)

Wymaga Node.js 20+.

cd backend && npm install && JWT_SECRET=dev DATABASE_PATH=./data/app.db npm start
cd frontend && npm install && npm run dev   # serwer dev na :5173, proxy /api -> :3000