# 04. Role, uprawnienia i izolacja salonów

## Dwa poziomy dostępu

| Poziom | Kto | Gdzie | Tabela kont |
|---|---|---|---|
| Operator | Właściciel systemu | `/admin` | `platform_admins` |
| Salon | Właścicielka i pracownicy | cała reszta panelu | `users` |

Te dwa światy nie mają wspólnej tabeli kont i wspólnej sesji. Konto pracownika salonu nie może przez żaden błąd w roli zyskać dostępu do panelu operatora, bo panel operatora w ogóle nie sprawdza tabeli `users`.

## Izolacja salonów

To najważniejsza reguła bezpieczeństwa w całym systemie. Wyciek danych między salonami jest jedyną awarią, po której produkt nie wraca do gry.

### Reguła

Każde zapytanie do tabeli z kolumną `salon_id` musi zawierać warunek `salon_id = :salon_id` z wartością pobraną z sesji, nigdy z parametru żądania.

### Jak to wymuszamy

1. **Klasa `Tenant`.** Po zalogowaniu ustawia `salon_id` w sesji. `Tenant::id()` jest jedynym źródłem tej wartości w całej aplikacji.
2. **Metoda bazowa repozytorium.** Każde repozytorium dziedziczy po `TenantRepository`, którego metoda `scoped(string $sql)` dokleja warunek i podstawia parametr. Ręczne `$db->query()` w module jest odstępstwem wymagającym komentarza z uzasadnieniem.
3. **Pobieranie po identyfikatorze.** `findById($id)` zawsze wykonuje `WHERE id = :id AND salon_id = :salon_id`. Klientka z innego salonu nie jest błędem 403, tylko 404, żeby nie potwierdzać istnienia rekordu.
4. **Test, który musi być w każdym etapie.** Dwa salony, po jednym rekordzie w każdej tabeli. Zalogowany użytkownik salonu A próbuje otworzyć każdy adres z identyfikatorem rekordu salonu B. Oczekiwany wynik: 404 wszędzie. Ten test jest punktem kryteriów odbioru każdego etapu, który dotyka danych.

## Role

Rola to zestaw domyślnych uprawnień. Właścicielka może je zmienić dla konkretnej osoby, a wyjątki lądują w `user_permissions`.

| Rola | Dla kogo | Domyślnie |
|---|---|---|
| `owner` | Właścicielka | Wszystko, bez możliwości odebrania |
| `manager` | Kierowniczka | Wszystko poza abonamentem, usuwaniem salonu i zmianą roli właścicielki |
| `staff` | Pracownica | Swój kalendarz, swoje wizyty, dostęp do klientek, bez finansów i bez ustawień |
| `reception` | Recepcja | Cały kalendarz, wszystkie wizyty, klientki, bez finansów, bez statystyk, bez ustawień |

Salon ma dokładnie jedno konto `owner`. Przekazanie własności to osobna akcja z potwierdzeniem hasłem.

## Katalog uprawnień

Klucze żyją w `config/permissions.php`. Nazwa klucza to `zasob.czynnosc`.

### Kalendarz i wizyty

| Klucz | Znaczenie |
|---|---|
| `calendar.view_own` | Widzi swój kalendarz |
| `calendar.view_all` | Widzi kalendarz wszystkich pracowników |
| `appointments.create` | Dodaje wizyty |
| `appointments.edit_own` | Edytuje swoje wizyty |
| `appointments.edit_all` | Edytuje wizyty innych |
| `appointments.delete` | Usuwa wizyty |
| `appointments.change_status` | Zmienia status, w tym oznacza nieobecność |
| `appointments.change_price` | Zmienia cenę i rabat na wizycie |

### Klientki

| Klucz | Znaczenie |
|---|---|
| `clients.view` | Widzi listę i karty klientek |
| `clients.create` | Dodaje klientki |
| `clients.edit` | Edytuje dane |
| `clients.delete` | Usuwa klientki |
| `clients.view_notes` | Widzi notatki wewnętrzne |
| `clients.export` | Eksportuje bazę do pliku |

### Oferta i zespół

| Klucz | Znaczenie |
|---|---|
| `services.manage` | Zarządza usługami i kategoriami |
| `staff.view` | Widzi listę pracowników |
| `staff.manage` | Dodaje, edytuje i usuwa pracowników |
| `staff.permissions` | Zmienia uprawnienia |
| `schedules.manage` | Ustawia grafiki i urlopy |

### Pieniądze i dane

| Klucz | Znaczenie |
|---|---|
| `finance.view` | Widzi przychody i kwoty |
| `stats.view_own` | Widzi swoje statystyki |
| `stats.view_all` | Widzi statystyki całego salonu |
| `billing.manage` | Zarządza abonamentem i płatnościami |

### SMS i rezerwacje

| Klucz | Znaczenie |
|---|---|
| `sms.send` | Wysyła pojedyncze wiadomości |
| `sms.send_bulk` | Wysyła do grupy klientek |
| `sms.templates` | Edytuje szablony |
| `sms.buy` | Kupuje pakiety |
| `settings.manage` | Zmienia ustawienia salonu |
| `booking.manage` | Konfiguruje rezerwacje online |

## Domyślne uprawnienia ról

| Uprawnienie | owner | manager | reception | staff |
|---|:---:|:---:|:---:|:---:|
| `calendar.view_own` | tak | tak | tak | tak |
| `calendar.view_all` | tak | tak | tak | nie |
| `appointments.create` | tak | tak | tak | tak |
| `appointments.edit_own` | tak | tak | tak | tak |
| `appointments.edit_all` | tak | tak | tak | nie |
| `appointments.delete` | tak | tak | nie | nie |
| `appointments.change_status` | tak | tak | tak | tak |
| `appointments.change_price` | tak | tak | nie | nie |
| `clients.view` | tak | tak | tak | tak |
| `clients.create` | tak | tak | tak | tak |
| `clients.edit` | tak | tak | tak | tak |
| `clients.delete` | tak | tak | nie | nie |
| `clients.view_notes` | tak | tak | tak | tak |
| `clients.export` | tak | tak | nie | nie |
| `services.manage` | tak | tak | nie | nie |
| `staff.view` | tak | tak | tak | tak |
| `staff.manage` | tak | tak | nie | nie |
| `staff.permissions` | tak | nie | nie | nie |
| `schedules.manage` | tak | tak | tak | nie |
| `finance.view` | tak | tak | nie | nie |
| `stats.view_own` | tak | tak | tak | tak |
| `stats.view_all` | tak | tak | nie | nie |
| `billing.manage` | tak | nie | nie | nie |
| `sms.send` | tak | tak | tak | nie |
| `sms.send_bulk` | tak | tak | nie | nie |
| `sms.templates` | tak | tak | nie | nie |
| `sms.buy` | tak | nie | nie | nie |
| `settings.manage` | tak | tak | nie | nie |
| `booking.manage` | tak | tak | nie | nie |

## Sprawdzanie w kodzie

```php
// Middleware na trasie
$router->get('/statystyki', [StatsController::class, 'index'])
       ->middleware('permission:stats.view_all');

// W kontrolerze, kiedy decyzja zależy od danych
if (!Auth::can('appointments.edit_all') && $appointment->user_id !== Auth::id()) {
    throw new ForbiddenException();
}

// W widoku, do ukrycia elementu interfejsu
<?php if (Auth::can('finance.view')): ?>
    <div class="kpi"><?= Money::format($revenue) ?></div>
<?php endif; ?>
```

Ukrycie przycisku w widoku nie jest zabezpieczeniem. Każda akcja sprawdza uprawnienie po stronie serwera, niezależnie od tego, co widać w interfejsie.

## Ograniczenia planu abonamentowego

Uprawnienia odpowiadają na pytanie "czy ta osoba może", plan odpowiada na pytanie "czy ten salon ma wykupione". To dwa niezależne sprawdzenia i oba muszą przejść.

| Funkcja | BASIC | PRO |
|---|---|---|
| Kalendarz, klientki, wizyty, usługi | tak | tak |
| Liczba kont pracowników | 1 | bez limitu |
| Uprawnienia pracowników | nie | tak |
| Statystyki | podstawowe | pełne |
| Automatyczne SMS-y | nie | tak |
| Historia klientki | ostatnie 3 wizyty | pełna |
| Rezerwacje online | nie | tak |

W kodzie: `Plan::has('sms.automatic')`. Przy braku dostępu użytkownik widzi funkcję, ale wyszarzoną, z informacją, co daje plan PRO. Ukrywanie funkcji całkowicie sprawia, że nikt nie wie, za co miałby dopłacić.

## Blokady po wygaśnięciu abonamentu

Konto nie znika po nieopłaceniu. Kolejność jest taka:

| Dzień od nieudanej płatności | Co się dzieje |
|---|---|
| 0 | E-mail o nieudanej płatności, pasek ostrzegawczy w panelu, pełny dostęp |
| 3 | Drugi e-mail, pasek zmienia kolor |
| 7 | Tryb tylko do odczytu: kalendarz i klientki widoczne, dodawanie zablokowane, automatyczne SMS-y wstrzymane |
| 30 | Konto zawieszone, logowanie prowadzi do strony płatności |
| 90 | Dane oznaczone do usunięcia, informacja mailowa 14 dni wcześniej |

Dane klientek nigdy nie są kasowane bez uprzedzenia i bez możliwości eksportu przez właścicielkę.
