Phase 1 umožnila anonymní podání ticketu přes Quick Ticket formulář — klient zadá jméno, email a popis problému, systém vrátí ticket_code. Po podání ale klient nemá žádný způsob jak sledovat stav, přidávat komentáře, nebo nahlížet na historii svých požadavků. Každý re-visit je anonymní. Bez Phase 2 musí agent ručně informovat klienta o každé změně. Cílem Phase 2 je dát klientovi jednoduché, zabezpečené rozhraní: registrace přes email, přihlášení, dashboard s vlastními tickety, detail ticketu.
Zvažovány tři možnosti:
Option A: Separátní tabulka client_account — vlastní user tabulka jen pro klienty. Čisté oddělení, ale duplicitní auth logika (session, login form, hash), nutnost dvou session mechanismů.
Option B: Nette Identity + separátní authenticator — klientský authenticator vedle agentského. Stále sdílí session systém, ale vyžaduje dvě identity a přepínání v BasePresenter.
Option C (zvoleno): Sdílený podpora21.user s role='client' — klientský account je standardní user řádek se role='client'. Existující auth (UserAuthenticator, session, BasePresenter) funguje beze změny. Přidáme pouze: (1) CHECK constraint na role = 'client', (2) FK support_client.user_id, (3) PortalPresenter s RBAC guardou role IN ('client'), (4) registrační flow find-or-create.
Výhody Option C: minimální změny v auth infrastruktuře, jeden session mechanismus, sdílený password hash, přímá vazba klient-account přes FK.
Budoucí migrace na Keycloak/SAML/OIDC: sdílený user model s role='client' usnadní případnou budoucí migraci — OIDC sub claim se mapuje na user.id, roles zůstávají. Authenticator se vymění za OIDC bridge, ostatní kód zůstane.
Požadované změny v DB:
1. podpora21.user.role — rozšíření CHECK constraint: přidat 'client' do povolených hodnot. Aktuální: CHECK (role IN ('agent', 'admin')). Nové: CHECK (role IN ('agent', 'admin', 'client')).
2. podpora21.support_client.user_id — nový nullable FK na podpora21.user(id). Propojuje anonymní support_client záznam s registrovaným účtem. NULL = klient nemá účet. Populated při registraci nebo při prvním přihlášení (find-or-create).
3. Indexy: support_client(user_id) pro rychlý lookup ticketů přihlášeného klienta.
Žádné jiné tabulky se nemění. Ticket tabulka zůstává beze změny — tickety jsou stále vázány na client_id (support_client).
GET /portal → Portal:default — landing, redirect na dashboard nebo login
GET /portal/login → Portal:login — přihlašovací formulář klienta
POST /portal/login → Portal:login! — zpracování přihlášení
GET /portal/register → Portal:register — registrační formulář
POST /portal/register → Portal:register! — zpracování registrace + find-or-create client
GET /portal/dashboard → Portal:dashboard — seznam ticketů přihlášeného klienta
GET /portal/ticket/<code> → Portal:ticket — detail ticketu přes ticket_code
GET /portal/profile → Portal:profile — profil klienta (jméno, email, heslo)
POST /portal/logout → Portal:logout! — odhlášení
Registrace klienta: klient zadá email + heslo (+ volitelně jméno).
find-or-create logic (transakční):
1. Hledej support_client WHERE tenant_id = current_tenant AND email = input_email.
2. Pokud existuje a user_id IS NOT NULL → účet již existuje, přesměruj na login s flash.
3. Pokud existuje a user_id IS NULL → vytvoř user záznam (role='client'), UPDATE support_client SET user_id = new_user.id.
4. Pokud neexistuje → INSERT support_client + INSERT user + FK update, vše atomicky.
5. Po úspěchu: přihlásit uživatele (Nette Identity), přesměrovat na /portal/dashboard.
Hash hesla: password_hash($password, PASSWORD_BCRYPT) — stejný způsob jako agentský user.
V Phase 2 není email verifikace — pro jednoduchost přijmeme any valid email. Phase 3 může přidat email confirmation token.
Dashboard zobrazuje tickety přihlášeného klienta.
Query:
SELECT t.*
FROM podpora21.support_ticket t
JOIN podpora21.support_client c ON c.id = t.client_id
WHERE t.tenant_id = :tenant_id
AND c.user_id = :logged_in_user_id
ORDER BY t.created_at DESC
LIMIT 50
Bezpečnostní invariant: dvě podmínky — tenant_id (tenant isolation) + c.user_id (client isolation). Klient nikdy nevidí tickety jiného klienta ani jiného tenantu.
Zobrazení: tabulka s ticket_number, ticket_code, title, status badge, created_at, link na detail. Stránkování v Phase 3.
Route: /portal/ticket/<code> kde <code> = ticket_code (6 znaků).
Autorizace: ticket musí patřit přihlášenému klientovi — ticket.client_id → support_client.id WHERE user_id = logged_in_user.id AND tenant_id = current_tenant. Při neúspěchu: 403 nebo redirect na dashboard.
Zobrazení: readonly pohled — title, description, status badge, created_at, resolved_at. Komentáře (thread) = Phase 3. Žádné action buttons v Phase 2 (klient nemůže měnit status, přidávat přílohy).
Cross-role guard: pokud je přihlášený agent/admin a navštíví /portal/*, přesměrovat na /helpdesk (nebo zobrazit 403).
Tenant isolation: každý DB dotaz obsahuje WHERE tenant_id = current_tenant_id
Client isolation: dashboard a detail filtrují na support_client.user_id = $user->id
Cross-role guard: PortalPresenter::startup() kontroluje role === 'client' — agent/admin je přesměrován pryč
RBAC guard v BasePresenter: existující checkAllowed() rozšíříme o resource 'portal' + action 'access' pro role 'client'
Password: bcrypt s cost=10, min 8 znaků na vstupu
CSRF: Nette Forms generují token automaticky pro všechny POST formuláře
Session: Nette session s httpOnly + secure (v produkci), SameSite=Lax
Rate limiting: Phase 3 (brute-force ochrana na /portal/login)
1. Migration (schema): ALTER TABLE support_client ADD COLUMN user_id BIGINT REFERENCES podpora21.user(id); ALTER TABLE podpora21.user DROP CONSTRAINT ...; ADD CONSTRAINT ... CHECK (role IN ('agent','admin','client'))
2. Migration (data): žádná — user_id začíná NULL pro všechny existující klienty
3. PortalModel: findClientByUserId(), findTicketsByClientId(), findTicketByCode(), createClientAccount() — vše tenant-scoped
4. PortalAuthenticator nebo rozšíření UserAuthenticator: ověřit login jen pro role='client' v /portal/* kontextu, nebo přijmout všechny role a přesměrovat dle role v BasePresenter
5. PortalPresenter: login, register, dashboard, ticket, profile, logout actions
6. Templates: portal/login.latte, portal/register.latte, portal/dashboard.latte, portal/ticket.latte, portal/profile.latte
7. Layout: nový portal-layout.latte (Bootstrap 4, jiný styl a navigace než agentský helpdesk — potvrzeno v review)
8. Router: přidat /portal/* routes do router.php
9. DI: registrovat PortalModel, PortalPresenter v services.neon
10. RBAC: přidat 'client' roli + 'portal' resource + 'access' action do ACL konfigurace
11. E2E seed: rozšířit DS-0005 seed migrace o klientský účet (role='client', user_id FK)
12. E2E testy: portal.spec.ts — register, login, dashboard shows own tickets, detail by code, 403 on other client's ticket
13. DAK: aktualizovat DS-0006 status na accepted po code review
Auth: sdílený UserAuthenticator pro /portal/login — Role='client' odděluje přístup přes RBAC, ne přes separátní authenticator. Budoucí migrace: Keycloak/SAML/OIDC — authenticator se vymění, zbytek zůstane.
Layout: nový portal-layout.latte — zákaznická sekce má jiný vizuální styl než agentský helpdesk (jiná navigace, barvy, branding).
Quick Ticket → 'Vytvořit účet': ano, chceme — implementujeme jako separátní Task + DS + ADR (mimo scope Phase 2). Smer potvrzen.
Email notifikace: budou delegovány na interní platformu mail21.cz (templates, compliance, rozesílání). Phase 2 může odeslat uvítací email přes mail21 API — upřesní se v separátním DS pro email integraci.
URL: /portal/ticket/<ticket_code> — potvrzeno, důvod viz DS-0004 (enumeration attack, nevyzrazování sekvenčního ID).
Bez email verifikace — klient může zadat libovolný email, Phase 3 přidá confirmation token
Bez OAuth / social login — jen email+heslo, Phase 3
Bez rate limitingu na /portal/login — brute-force ochrana v Phase 3
Klient nemůže v Phase 2 přidat komentář k ticketu — read-only pohled
Stránkování dashboardu není v Phase 2 — LIMIT 50, Phase 3
Zapomenuté heslo (password reset) není v Phase 2 — agent může resetovat ručně přes DB
Žádné emailové notifikace při změně stavu ticketu — Phase 3
| Version | Date | Author | Note |
|---|---|---|---|
| 0.1.0 | 2026-04-10 | david.sorf + claude-sonnet-4-6 | Initial draft — auth model (Option C: shared podpora21.user role=client), data-model (user_id FK), URL map (7 routes), registration find-or-create, security model, 13 implementation steps. |
| 0.1.1 | 2026-04-10 | david.sorf | Review decisions recorded: sdílený auth + budoucí Keycloak migrace; portal-layout.latte separátní; QT→účet jako separátní DS/ADR; email přes mail21.cz; URL přes ticket_code (viz DS-0004). |