Tenant = zákazník provozující vlastní instanci helpdesku v rámci podpora21 platformy.
Kanonická tabulka: public.core_tenant (existuje, zděděna z core migrace)
Seed záznam pro EP21:
name: 'ep21'
display_name: 'EP21 s.r.o.'
Tenant se resolví při každém requestu přes TenantResolverService — v fázi 1 staticky z konfigurace (TENANT_KEY=ep21 v .env). V fázi 2 z URL prefixu nebo subdomény.
| Tabulka | tenant_id? | Poznámka |
|---|---|---|
podpora21.user | ANO | Uživatelé (agenti i klienti) patří tenantu |
podpora21.support_ticket | ANO | Každý ticket patří tenantu |
podpora21.support_client | ANO | Klientský záznam je per-tenant |
podpora21.support_category | ANO | Kategorie definuje tenant |
podpora21.support_sla_policy | ANO | SLA politiky per-tenant |
podpora21.support_ticket_comment | NE | Izolováno přes ticket FK |
podpora21.support_ticket_history | NE | Izolováno přes ticket FK |
podpora21.rbac_role | NE | Sdílené globální role (fáze 1) |
podpora21.rbac_person_role | NE | Izolováno přes user FK |
public.core_tenant | N/A | Kořenová tabulka tenantů |
Singleton service registrovaná v DI. Resolví aktuálního tenanta pro každý request.
Fáze 1 (implementováno v TASK-0344):
Fáze 2 (plánováno):
Použití v Presenterech: @inject TenantResolverService, $this->tenantResolver->get()->getId()
Použití v Modelech: tenant_id se předává explicitně do každé operace — bezpečnější než implicitní.
| Migrace | Co | Phinx config | Závislost |
|---|---|---|---|
20260410020000_tenant_seed_ep21 | INSERT do core_tenant (name='ep21') | phinx-podpora21.php | žádná |
20260410030000_user_tenant_id | ADD tenant_id NOT NULL DEFAULT ep21.id na podpora21.user | phinx-podpora21.php | tenant seed |
20260410040000_helpdesk_tenant_id | ADD tenant_id na support_ticket, support_client, support_category, support_sla_policy | phinx-podpora21.php | user tenant_id |
20260410050000_helpdesk_tenant_rls (opt) | PostgreSQL RLS policies | phinx-podpora21.php | helpdesk tenant_id |
Všechny queries na tenant-owned tabulky MUSÍ obsahovat WHERE tenant_id = :tenant_id.
Vzor v TicketModel (po migraci):
$this->database->table(self::TAB_NAME)
->where('tenant_id', $this->tenantId)
->...
Vzor v raw SQL:
SELECT * FROM podpora21.support_ticket
WHERE tenant_id = ? AND ...
Bezpečnostní pravidlo: žádná query na tenant-owned tabulku bez tenant_id filtru.
V Quick Ticket formuláři (anonymní): tenant_id z TenantResolverService — klient nemusí znát tenanta.
| Typ | user_type | tenant_id | Přístup |
|---|---|---|---|
Agent / operátor | agent | ep21 | /helpdesk — plný helpdesk backend |
System admin | admin | ep21 | /helpdesk + /rbac-admin + /tek |
Klient (zákazník) | client | ep21 | /portal — vlastní tickety only |
Anonymní | — | — | / a /portal (read + quick ticket) |
| Fáze | Co | Task | Blokuje |
|---|---|---|---|
1a — DB | Tenant seed + tenant_id migrace (4 migrace) | TASK-0343 | vše ostatní |
1b — Service | TenantResolverService (statický z .env TENANT_KEY) | TASK-0344 | Quick Ticket, Model update |
1c — Model | TicketModel + CategoryModel + ClientModel přidají tenant_id do queries | TASK-0344 | Quick Ticket form |
1d — Portal | Landing page + /portal info + Quick Ticket form | TASK-0342 | TASK-0343 + TASK-0344 |
2a — Client auth | Klientský login, self-registrace, client user_type | budoucí | TASK-0342 done |
2b — Multi-tenant routing | Subdomain resolver nebo URL prefix | budoucí | ADR-0002 fáze 2 trigger |
Fáze 1: TenantResolver je statický (TENANT_KEY z .env) — žádný subdomain routing
rbac_role sdílené mezi tenants v fázi 1 — fáze 2 přidá tenant_id nebo per-tenant seed
RLS (Row Level Security) je volitelné — WHERE tenant_id je dostačující pro fázi 1
support_ticket_comment a support_ticket_history nemají přímý tenant_id — izolace přes ticket FK
Email notifikace (ticket confirmation) není v scope DS-0003
| Version | Date | Author | Note |
|---|---|---|---|
| 0.1.0 | 2026-04-10 | david.sorf + claude-sonnet-4-6 | Initial draft — row-level tenant isolation, migrační plán, TenantResolverService, query pattern. |