# Felhasználókezelés és a jogosultság-modell érvényesítése

**Dokumentum-típus:** feature-specifikáció
**Modul:** Admin felület → Beállítások → Felhasználók (`/beallitasok/felhasznalok`)
**Verzió:** v1.0
**Dátum:** 2026.05.19
**Cél olvasó:** senior fejlesztő + Claude Code
**Épít:** `00_domain_model.md`, `01_kozos_mintak.md`, `99_donesnaplo.md`,
`00_terminologia.md`, `CLAUDE.md`, valamint a `80_modulok/manager_felulet/`
funkcionális dokumentumok

> **Mire való ez a fájl.** Ez a feature-spec a városgazdálkodás belső
> felhasználóinak kezelését (`/beallitasok/felhasznalok`) és a meghívó-flow-t
> specifikálja fejlesztői mélységben. A jogosultság-érvényesítés
> **platform-szintű, feature-független** mintáját nem ez a fájl tartalmazza —
> az a `01_kozos_mintak.md` 8. szakaszában él; ez a feature-spec oda
> hivatkozik. A `CLAUDE.md` hézag-kezelési konvencióit (megtartott hiány,
> `NY-xxx`, `VF-xxx`) követi.

---

## 0. Funkcionális alap

**Mely funkcionális dokumentumok fedik ezt a feature-t.** A feature két, jól
elkülönülő rétegből áll, és a két réteg más-más funkcionális dokumentumra épül.

A *jogosultság-modell* (mit szabad kinek):

- `05_jogosultsagok_v2.md` 1. (szerepkör-keret), 2.1–2.9 (akció-szintű
  mátrix), 3. (negatív leltár), 4. (landing-logika) — **a fő bemenet.**
- `00_architektura_v4.md` 2.1–2.2 (szerepkörök és viszonyuk), 3.2–3.3
  (navigáció, URL), 4. (landing).
- `90_sitemap_v3.md` 2.6 (Beállítások nézet), 3. (jogosultság-térkép a
  nézetekhez).

A *felhasználókezelés* (a felhasználó-CRUD felülete):

- `50_konfig_v2.md` 4. (Felhasználók — 4.1 pilot-szint, 4.2 mi nincs), 3.
  (Csoportok — a csoporttagsággal való metszet), 6.2 (csoport-tagság honnan
  szerkeszthető), 7.1–7.2 (pilot-scope) — **a fő bemenet a felhasználó-CRUD-ra.**
- `90_sitemap_v3.md` 2.6 (`/beallitasok/felhasznalok` mint önálló-URL-ű nézet).
- `manager_felulet_atadas.md` — a belépő; megerősíti, hogy a Beállítások nem
  igényel új UI-komponenst.

**Mely kanonikus döntések érintettek.** K-016 (szerepkör-szintű, durva
szemcsés jogosultság a pilotra); K-024 (a super-admin funkciók külön
Urbino-admin felületé); K-025 (implicit unió-modell — nincs szerep-váltó UI);
K-027 (szerepkör-érzékeny landing redirect); K-038 (a tartalomkezelő mint
harmadik pilot-szerepkör). A felmérő kör `Ü-6` döntése (a felhasználó-felvétel
e-mail meghívóval történik) szintén központi — a meghívó token/lejárat/sablon
explicit a felhasználó-spec hatáskörébe utalva.

**Mit döntött már el a funkcionális réteg.** A jogosultság-modell
funkcionálisan **lezárt**: négy szerepkör (diszpécser, vezető, tartalomkezelő,
átmeneti terepi dolgozó), a `vezető ⊇ diszpécser ÉS ⊇ tartalomkezelő` reláció,
a teljes akció-szintű mátrix és a tételes negatív leltár, a szerepkörönkénti
landing-logika — mind a `05_jogosultsagok_v2.md`-ben. A `05` 5. szakasza
expliciten kimondja, hogy a konkrét technikai megvalósítás (permission-kulcsok,
claim-ek, API-szintű ellenőrzés) a Specifikációs projekt dolga. A
felhasználókezelés funkcionálisan szándékosan vékony (`50_konfig_v2.md` 4.):
felhasználó-CRUD (név, e-mail, szerepkör), szerepkör-hozzárendelés,
deaktiválás; a meghívás/aktiválás mechanizmusát a `50` 4.2 záró bekezdése
expliciten a Specifikációs projektnek adta át.

**Mit tölt ki EZ a specifikáció.** A jogosultság-modell implementációs
szintjét — a `05` akció-mátrixának leképezését `authorization.json`
route-szabályokra, a `TenantRole` enum kódbeli rögzítését, a kétrétegű
érvényesítés általánosított mintáját (ez a `01_kozos_mintak.md` 8.
szakaszába kerül, lásd 3.7); és a felhasználókezelés teljes fejlesztői
specifikációját: a `User`/`UserTenantRole` API-szerződését, a meghívó-flow-t
(ez az `NY-5` lezárása), a felhasználó-adatlap és -lista mezőszintű
részletezését, a validációt, a `User`-állapotgépet.

---

## 1. Cél és hatókör

A feature lehetővé teszi, hogy a vezető (Béla) a saját tenantján felvegye és
kezelje a városgazdálkodás munkatársait — meghívja őket, szerepkört rendeljen
hozzájuk, deaktiváljon kilépő dolgozót —, és biztosítja, hogy a platform
jogosultság-érvényesítése a teljes admin felületen működjön: minden szerepkör
pontosan azt érje el, amit a `05_jogosultsagok_v2.md` mátrixa megenged. Az
üzleti érték: a tenant önállóan, fejlesztői beavatkozás nélkül kezeli a saját
felhasználói körét, és a jogosultság-határok kikényszerítve állnak az első
naptól.

**Hatókörön kívül (ennél a feature-nél):**

- **Urbino-admin funkciók** — új tenant felvétele, billing, feature-flag, a
  `DefaultCategoryCatalog` CRUD-ja (K-024). A `Tenant` entitás itt adottság.
- **Polgári autentikáció** — a polgár nem `User` ebben az értelemben; a
  polgári fiók-modell külön kérdés (`NY-2` környéke, `02_kerdeslista` 4.).
- **Jelszó-politika, kétfaktoros beállítás, session-kezelés** — Zitadel-oldali;
  a `50_konfig_v2.md` 4.2 expliciten kizárta a funkcionális tervezésből, és
  ez a spec sem konfigurálja.
- **Csoport-CRUD** — a `Group` entitás kezelése a `/beallitasok/csoportok`
  aloldalé, külön feature-spec. Ez a feature csak annyit érint, hogy a
  felhasználó-adatlap a csoporttagságot olvasásra mutatja.
- **A `Tenant`-alapadatok** (`/beallitasok/altalanos`) — szintén külön feature.

---

## 2. Domain-modell

A feature három meglévő entitásra épül (`User`, `UserTenantRole`,
`TenantUser`), és egy újat vezet be (`UserInvitation`). A meglévő entitásokat
a `00_domain_model.md` 3. blokkja tartalmazza — ez a spec **hivatkozza**,
nem ismétli. Az új entitást és a `User`-állapotgépet itt rögzítjük; a
`00_domain_model.md` ezeket átveszi.

### 2.1 Áttekintés — mi új, mi módosul

| Elem | Státusz | DB-szint |
|---|---|---|
| `User` | meglévő — egy kapcsolat-pontosítás (`UserInvitation`) | Core DB |
| `UserTenantRole` | meglévő — változatlan | Core DB |
| `TenantUser` | meglévő — `status` mező az `isActive` helyett (SD-37) | Tenant DB |
| `UserInvitation` | **új entitás** | Core DB |
| `UserStatus` enum | meglévő — `Invited` / `Active` / `Disabled` | — |
| `TenantRole` enum | meglévő — `dispatcher` / `manager` / `content_manager` / `field_worker` | — |
| `InvitationStatus` enum | **új enum** — `Pending` / `Accepted` / `Expired` / `Revoked` | — |

### 2.2 Új entitás — `UserInvitation`

**DB-szint:** Core DB
**Származik:** `AuditableEntity`
**Kapcsolódó döntések:** SD-34 (külön entitás), SD-35 (lejárat és újraküldés),
`Ü-6` (e-mail meghívó); az `NY-5` lezárása.

A `UserInvitation` egyetlen meghívási kísérletet reprezentál: egy `User`-höz
tartozó, lejáratos, egyszer használatos token.

| Mező | Típus | Köt. | Megjegyzés |
|---|---|---|---|
| `id` | `long` (PK) | K | — |
| `userId` | `FK → User` | K | Melyik felhasználóhoz tartozik a meghívó |
| `tenantId` | `FK → Tenant` | K | Melyik tenant kontextusában keletkezett a meghívó. Egy `User` több tenanton is meghívható; a meghívó tenant-specifikus |
| `tokenHash` | `string` | K | A meghívó-token **hash-elt** alakja. A nyers token csak az e-mailbe kerül, a DB-ben sosem. Hossz: a választott hash-algoritmus kimenete (fejlesztői döntés — pl. SHA-256 → 64 karakter) |
| `expiresAt` | `DateTime` (UTC) | K | A token lejárati időbélyege. Létrehozáskor `createdAt + 7 nap` (SD-35). UTC-ben tárolva (SD-9) |
| `status` | `enum InvitationStatus` | K | A meghívó állapota — lásd lent. **Default: `Pending`** |
| `consumedAt` | `DateTime` (UTC) | O | Mikor váltották be. Csak `Accepted` státuszban kitöltött; egyébként üres |

**Az `InvitationStatus` értékkészlete (új enum):**

| Érték | Jelentés |
|---|---|
| `Pending` | Kiküldve, érvényes, még nem váltották be és nem járt le |
| `Accepted` | A felhasználó beváltotta — a `User` ezzel `Active` lett |
| `Expired` | Lejárt beváltás nélkül (`expiresAt` elmúlt) |
| `Revoked` | Érvénytelenített — tipikusan újraküldéskor a korábbi meghívó, vagy ha a `User`-t a beváltás előtt deaktiválták |

> **Tervezési döntés — a meghívó külön entitás (SD-34).** A meghívásnak saját
> életciklusa van: kiküldik, lejár, újraküldik (új token, régi érvénytelen),
> beváltják. Ha ezt a `User` mezőin tárolnánk, az újraküldés felülírná az
> előzményt, és nem lenne nyoma, hányszor és mikor hívták meg a felhasználót.
> Külön entitásként a meghívási kísérletek sora megmarad (egy `User`-höz több
> `UserInvitation` is tartozhat időben), és a tábla `AuditableEntity` — a
> `createdBy` rögzíti, ki (melyik vezető) küldte a meghívót. *(Döntésnapló:
> SD-34.)*

> **Az `Expired` állapot beállásáról.** A `Pending → Expired` átmenet
> idő-vezérelt, nem akció-vezérelt — nincs felhasználói művelet mögötte. A
> pilotra **lazy ellenőrzés** elég: a beváltáskor, ha `expiresAt < now`, a
> `status` `Expired`-re frissül, és a beváltás `410`-zel elutasul. Időzített
> háttér-job iterációba kerülhet (8.2).

> **Egyedi-megkötés.** Egy `User`-höz egy adott tenanton legfeljebb egy
> `Pending` meghívó tartozhat. Ezt nem adatbázis-szintű unique-constraint
> adja (a régi `Revoked`/`Expired` sorok megmaradnak), hanem a meghívás-indító
> logika: új meghívó kiküldése előtt a tenant korábbi `Pending` meghívóját
> ugyanarra a `User`-re `Revoked`-ra állítja (lásd 3.5).

### 2.3 Kapcsolat-pontosítás — `User`

A `User` entitás mezőkészlete nem változik. Egyetlen pontosítás: a `User` 1-n
kapcsolatban áll a `UserInvitation`-nel (`UserInvitation.userId → User`). A
`User.status = Invited` és az `externalAuthId` feltételes üressége
(`00_domain_model.md` 3.1) az `NY-5` lezárásával kap pontos folyamatot: az
`externalAuthId` a meghívó **beváltásakor** töltődik ki, a Zitadel-oldali
fióklétrehozás eredményéből. A részletes flow a 3.6 API-szakaszban és a 2.6
állapotgépben.

### 2.4 Módosítás — `TenantUser` (SD-37)

A `TenantUser` a Core `User` szűk, csak-olvasható projekciója a Tenant DB-ben
(SD-2). A TD-B5 döntés nyomán a `TenantUser` a meghívott (`Invited`)
felhasználót **is** projektálja — nem csak az aktívakat —, hogy a tenant-oldali
lekérdezések (felhasználó-lista, a Dashboard csapat-nézete) lássák a még nem
aktivált kollégát.

> **Tervezési döntés — a `TenantUser` állapot-tükrözése (SD-37).** A
> `TenantUser` korábbi bináris `isActive` (bool) mezeje **`status` enumra**
> változik, amely a Core `UserStatus`-t tükrözi (`Invited` / `Active` /
> `Disabled`). Indok: egy `Invited` felhasználó se nem „aktív", se nem
> „letiltott" — a bináris mező nem tudja megkülönböztetni. Mivel a feature az
> `Invited` felhasználót is projektálja, a tenant-oldali kód (pl. a Dashboard
> csapat-nézete) jól jár, ha az állapotot is látja anélkül, hogy a Core-hoz
> kéne fordulnia. Ez a `00_domain_model.md` 2.3 `TenantUser`-sémájának és az
> SD-2-nek a pontosítása. *(Döntésnapló: SD-37.)*

### 2.5 A meglévő `User` / `UserTenantRole` — hivatkozás

A `User` (`00_domain_model.md` 3.1) globális felhasználói fiók: `id`,
`displayName`, `email`, `status` (`UserStatus`), `externalAuthId`. A
`UserTenantRole` (`00_domain_model.md` 3.3) a `User ↔ Tenant` explicit
kötés-entitás `userId`, `tenantId`, `role` (`TenantRole`) mezőkkel; egy
`(userId, tenantId, role)` hármas egyedi (SD-20). A jelszó nincs a `User`-ön —
azt a Zitadel kezeli. Ezeket a feature **átveszi**, nem módosítja.

### 2.6 Állapotgép — a `User` életciklusa

A `User` életciklusa kisebb, mint a `Ticket`-é, ezért nem a
`02_globalis_allapotgep.md`-be kerül, hanem itt él. A formális átmenet-tábla:

| Honnan | Hova | Kiváltó akció | Jogosultság | Feltétel / mellékhatás |
|---|---|---|---|---|
| — | `Invited` | Felhasználó meghívása (`/beallitasok/felhasznalok` „Meghívás") | `manager` | Létrejön a `User` (`status=Invited`) és egy `Pending` `UserInvitation`; legalább egy `UserTenantRole` (SD-38); meghívó-e-mail kimegy |
| `Invited` | `Active` | A meghívó beváltása (a felhasználó a linkről aktivál) | a meghívott maga (tokennel) | A `UserInvitation` `Pending` és nem járt le; a Zitadel-fiók létrejön; `externalAuthId` kitöltődik; a `UserInvitation` `Accepted` |
| `Invited` | `Disabled` | Meghívott felhasználó deaktiválása beváltás előtt | `manager` | A `Pending` `UserInvitation` `Revoked`-ra áll |
| `Active` | `Disabled` | Felhasználó deaktiválása | `manager` | SD-38: nem lehet az utolsó aktív `manager`; nem deaktiválhatja önmagát |
| `Disabled` | `Active` | Felhasználó újraaktiválása | `manager` | Ha van `externalAuthId` (volt már aktív) → nem kell új meghívó. Ha nincs (sosem aktivált `Invited`-ből letiltott) → új meghívó kell, lásd lent |

> **A `Disabled → Active` finomsága.** Két eset rejlik benne: (1) egy *valaha
> aktív* felhasználót reaktiválunk — van `externalAuthId`-ja, a Zitadel-fiók
> létezik, egyszerű állapot-visszaállítás; (2) egy *sosem aktivált*
> (`Invited`-ből `Disabled`-ba került) felhasználót „élesztünk újra" — nincs
> `externalAuthId`, ehhez új meghívó kell, tehát a `Disabled → Invited →
> Active` úton megy. Az API-szakasz (3.6) ezt a két utat szétválasztja.

> **Nincs `Active → Invited`.** Aktív, már belépett felhasználót nem lehet
> „visszameghívni" — a Zitadel-fiók megvan. Jelszó-újraküldés,
> fiók-helyreállítás Zitadel-oldali, nem `User`-állapotgép.

A `UserInvitation` állapotgépe ehhez kötött és egyszerű: `Pending → Accepted`
(beváltás), `Pending → Expired` (lejárat), `Pending → Revoked` (újraküldés
vagy a `User` deaktiválása). Az `Accepted` / `Expired` / `Revoked`
végállapotok.

### 2.7 Lookup-ok, default katalógus-érintettség

A feature nem érint default katalógust. A `TenantRole` enum a felhasználó-lista
szerepkör-szűrőjéhez és az adatlap szerepkör-választójához lookup-ként
viselkedik — de termékesített, fix négyértékű enum, nem `ILookupProvider`-rel
kezelt tenant-adat.

---

## 3. Szerver — API és logika

A vezérelv a `01_kozos_mintak.md` 6.4 „csak az eltérést specifikáld" elve.

### 3.1 Mi standard, mi tér el

| Entitás / művelet | Minta | Megjegyzés |
|---|---|---|
| `UserTenantRole` — olvasás | Standard `BaseController` (list, get) | A felhasználó-adatlap szerepkör-listájához |
| `UserInvitation` — olvasás | Standard `BaseController` (list, get) | A felhasználó-adatlap meghívó-előzményéhez; írásra nem publikus |
| `User` — list, get | Standard `BaseController`, **eltéréssel**: tenant-szűrés (SD-36) | Lásd 3.2 |
| `User` — create | **Eltérés a mintától** — nem sima `POST`, hanem meghívás-indítás | Lásd 3.3 |
| `User` — update | Standard update, **eltéréssel**: az állapot- és `email`-mező nem szabadon írható | Lásd 3.4 |
| `User` — delete / bulk delete | **Nincs** — a `User` nem törölhető (SD-39) | Lásd 3.4 |
| Meghívás-indítás, beváltás, újraküldés, deaktiválás, reaktiválás | **Eltérés a mintától** — önálló akció-végpontok | Lásd 3.3–3.5 |

> **Architekturális keret.** A `User`, `UserTenantRole`, `UserInvitation`
> Core-entitások — a Core DbContext nem megy át a tenant-resolution
> middleware-en (SD-6). A felhasználó-kezelő végpontok ezért egy dedikált
> controllerbe kerülnek (munkanév: `UserManagementController`), amely a Core
> DbContext-et használja, de a `Tenant` headerből kiolvasott tenant-kódra
> explicit szűr minden lekérdezésnél (SD-36). A konkrét osztály-struktúra
> fejlesztői döntés; a spec a végpont-szerződéseket köti.

### 3.2 `User` — list és get

**Standard CRUD a `BaseController` szerint**, két eltéréssel:

> **Eltérés a mintától — tenant-szűrés (SD-36).** A `GET /v1/users` és
> `GET /v1/users/{id}` nem a teljes Core `User`-halmazt adja, hanem azokat a
> `User`-eket, akiknek van `UserTenantRole`-juk az **aktív tenanton** (a
> `Tenant` headerből). A vezető csak a saját tenantja felhasználóit kezelheti;
> cross-tenant `User` nem szivároghat a listába. Egy idegen tenant `User`-ének
> `id`-jára a `GET /v1/users/{id}` **`404`** — a `01_kozos_mintak.md` 6.3
> „idegen tenant erőforrás → 404" mintája szerint.

> **Eltérés a mintától — a list-DTO szerepkör- és állapot-aggregátuma.** A
> `UserListDto` a standard mezőtükrözés mellett két származtatott mezőt
> hordoz: a felhasználó **szerepköreinek listáját ezen a tenanton** (a
> `UserTenantRole`-sorokból aggregálva) és a meghívó-állapotot (`Invited`
> felhasználónál van-e `Pending` `UserInvitation`, lejárt-e). A felhasználó-
> lista ezeket oszlopként mutatja (4.2) — ne a kliens számolja külön hívásból.

A többi — pagináció, `FilterQuery`, rendezés, oszlop-láthatóság — a standard
`ListRequest` / `TableStateConfig` minta.

### 3.3 Meghívás-indítás — `POST /v1/users/invite`

> **Eltérés a mintától.** A `BaseController` `create` művelete egy entitást
> ír. Itt egy `create` három dolgot vált ki egy tranzakcióban: létrejön a
> `User` (`status=Invited`), létrejön legalább egy `UserTenantRole`, és
> létrejön egy `Pending` `UserInvitation` — majd egy meghívó-e-mail kimegy.
> Ez workflow, nem CRUD. A logika a `UserManagementController` `invite`
> action-methodjében, illetve egy dedikált service-ben (munkanév:
> `UserInvitationService`) — a konkrét struktúra fejlesztői döntés.

**Route:** `POST /v1/users/invite`
**Method:** `POST`
**Jogosultság:** `manager` (lásd 3.8)
**Tenant-kontextus:** a `Tenant` header adja, melyik tenantra szól a meghívás

**Request — `InviteUserRequest`:**

| Mező | Típus | Köt. | Megjegyzés |
|---|---|---|---|
| `email` | `string` | K | A meghívott e-mail címe |
| `displayName` | `string` | K | A megjelenítendő név |
| `roles` | `TenantRole[]` | K | Legalább egy szerepkör (SD-38). Duplikátummentes |

**Válasz:** `201 Created`, a létrejött felhasználó `UserDetailDto`-ja
(`status=Invited`, a `Pending` meghívó adataival). Ha a `User` már létezett és
aktív (lásd workflow 3. pont), a válasz `200 OK`.

**A workflow lépései (egy tranzakcióban):**

1. **E-mail-feloldás.** Ha az `email` már létezik a Core `User`-ben: nem jön
   létre új `User` — a meglévőhöz csak új `UserTenantRole`-sor(ok) keletkeznek
   erre a tenantra. Ha nem létezik: új `User` jön létre (`status=Invited`).
2. **Szerepkör-hozzárendelés.** A `roles` minden eleméhez egy `UserTenantRole`-sor
   (`userId`, a `Tenant` header tenantja, `role`).
3. **Meghívó.** Új `UserInvitation` (`Pending`, `expiresAt = now + 7 nap`,
   friss token). Ha a `User` **már aktív** (létező, aktív felhasználót adtak új
   tenanthoz): meghívó nem kell — a felhasználó már be tud lépni, csak az új
   tenant jelenik meg neki. Ekkor a válasz `200 OK`, és nincs `UserInvitation`.
4. **E-mail.** A meghívó-e-mail kimegy a nyers tokennel. „Best effort": ha az
   e-mail-küldés hibázik, az nem görgeti vissza a tranzakciót (a
   `User`/`UserTenantRole`/`UserInvitation` megmarad) — a vezető a felületről
   újraküldheti.

**Hibakódok:**

| Kód | Eset |
|---|---|
| `400` | Validáció — `fieldErrors` (üres `displayName`, rossz `email`-formátum, üres `roles`) |
| `403` | A hívónak nincs `manager` szerepköre az aktív tenanton |
| `409` | Az `email` már létezik **és van `UserTenantRole`-ja ezen a tenanton** — a felhasználó már tagja ennek a tenantnak. `reason`-kód a kliensnek |
| `422` | A `roles` érvénytelen kombináció (a pilotra nincs ilyen, de a keret nyitva) |

### 3.4 `User` — update, deaktiválás, reaktiválás, törlés

**`PUT /v1/users/{id}` — standard update, két eltéréssel:**

> **Eltérés a mintától — a `status` nem szabadon írható.** A `User.status`
> átmeneteit a `User`-állapotgép (2.6) szabályozza, nem a sima update. A `PUT`
> a `displayName`-et és — közvetve, a `UserTenantRole`-okon át — a
> szerepköröket módosítja; a `status`-t nem. Az állapot-átmenetek dedikált
> végpontokon mennek (deaktiválás, reaktiválás, beváltás).

> **Eltérés a mintától — az `email` nem módosítható.** Az `email` egyúttal a
> Zitadel-bejelentkezés azonosítója (`00_domain_model.md` 3.1). A pilotra az
> `email` módosítását kizárjuk — Zitadel-oldali identitás-átkötést igényelne.
> A felület az `email`-mezőt csak olvasásra mutatja aktivált felhasználón.
> *(Tudatos kihagyás — 8.2.)*

A szerepkörök szerkesztése (`UserTenantRole`-sorok hozzáadása/elvétele) a
`PUT` része; a UI-modell az 5.4-ben. A SD-38 korlátok itt is érvényesek
(lásd 3.6 validáció).

**`POST /v1/users/{id}/deactivate` — deaktiválás:**

**Jogosultság:** `manager`. **Hatás:** `User.status` → `Disabled`. Ha a
felhasználó `Invited` volt, a `Pending` `UserInvitation` → `Revoked`. A
`TenantUser`-projekció `status`-a frissül (SD-37).

**Hibakódok:** `403` (nem `manager`); `422` két esetben — (a) a hívó saját
magát deaktiválná; (b) a felhasználó az utolsó aktív `manager` ezen a
tenanton (SD-38). Mindkettőnél `reason`-kód.

**`POST /v1/users/{id}/reactivate` — reaktiválás:**

**Jogosultság:** `manager`. **Hatás:** a 2.6 állapotgép `Disabled → Active`
átmenete. Ha a felhasználónak van `externalAuthId`-ja (volt már aktív):
`status` → `Active`, kész. Ha nincs (sosem aktivált, `Invited`-ből letiltott):
a végpont `422`-t ad `reason`-kóddal („új meghívó szükséges") — a vezető ekkor
az `invite`/újraküldés úton viszi tovább.

**Törlés — nincs.**

> **Eltérés a mintától — nincs delete (SD-39).** A `User.id`-ra
> `Ticket.createdBy`/`updatedBy`, `ActivityLog.actorId`, `TicketNote.authorId`
> hivatkozik. Egy `User` törlése elárvult hivatkozásokat csinálna. A
> felhasználó „eltávolítása" mindig deaktiválás (`Disabled`) — az adatok és a
> hivatkozások megmaradnak. A `BaseController` `delete` és `bulk delete`
> műveletei a `UserManagementController`-en nem elérhetők. *(Konzisztens a
> `Category` „törölni nem, csak deaktiválni" elvével, `50_konfig` 2.4.)*

### 3.5 Meghívó beváltása és újraküldése

**`POST /v1/users/invitations/accept` — a meghívó beváltása:**

Ez a végpont a meghívott felhasználó aktiválását végzi. **Speciális a
jogosultsága:** nem szerepkör-védett — a token maga az azonosítás. A
felhasználó még nem létezik a Zitadelben, JWT-je sincs.

**Request — `AcceptInvitationRequest`:**

| Mező | Típus | Köt. | Megjegyzés |
|---|---|---|---|
| `token` | `string` | K | A nyers meghívó-token az e-mail linkjéből |

**A workflow:**

1. A `token` hash-elt alakja alapján a `UserInvitation` megkeresése.
2. Érvényesség: a meghívó `Pending`, és `expiresAt > now`. Ha lejárt: a
   `status` `Expired`-re frissül (lazy ellenőrzés, 2.2), a válasz `410 Gone`
   `reason`-kóddal.
3. A Zitadel-oldali fiók létrejön (a `User.email`-lel); az `externalAuthId`
   kitöltődik a Zitadel-azonosítóval.
4. `User.status` → `Active`; `UserInvitation.status` → `Accepted`,
   `consumedAt` kitöltve.
5. A `TenantUser`-projekció `status`-a frissül `Active`-ra (SD-37).

> **Megtartott hiány — a beváltás UX-folyamata.** A beváltás *technikai*
> láncát (token → Zitadel-fiók → `Active`) a spec rögzíti. De hogy a
> felhasználó **mit lát** a beváltás közben — egy admin-felületi aktiváló-
> oldalt jelszó-megadással, vagy a Zitadel saját onboarding-képernyőjét — ez
> felületi folyamat, amit funkcionálisan nem terveztek meg. A `CLAUDE.md`
> „megtartott hiány" kategóriája szerint: a feature halad tovább (a beváltás
> API-lánca kész, a gerinc nem sérül), de a beváltó-képernyő UX-e megtartott
> hiányként rögzül (8.4). Valószínű irány a Zitadel saját jelszó-állító
> képernyője; ezt fejlesztői + funkcionális tisztázás zárja le.

> **Zitadel-hiba a beváltáskor.** Ha a 3. lépés (Zitadel-fiók létrehozása)
> hibázik, a tranzakció visszagördül: a `User` `Invited` marad, a
> `UserInvitation` `Pending` marad — a felhasználó újrapróbálhatja a linkkel.
> A válasz `502`/`503` jellegű, `reason`-kóddal. A konkrét hibakód-leképezés a
> fejlesztői Zitadel-integráció része.

**`POST /v1/users/{id}/invitations/resend` — meghívó újraküldése:**

**Jogosultság:** `manager`. **Hatás:** a `User`-höz tartozó korábbi `Pending`
`UserInvitation` ezen a tenanton → `Revoked`; új `UserInvitation` jön létre
(`Pending`, friss `expiresAt = now + 7 nap`, új token); a meghívó-e-mail újra
kimegy. Csak `Invited` állapotú felhasználóra értelmezett.

**Hibakódok:** `403` (nem `manager`); `422`, ha a felhasználó nem `Invited`
állapotú (`reason`-kód).

### 3.6 Validáció és business-szabályok

A mezőméreteket a `00_domain_model.md` rögzíti — a spec hivatkozza, nem
másolja. A teljes szabálykészlet (FluentValidation szinten):

**`InviteUserRequest`:**

- `email` — kötelező; e-mail-formátum; a Core `User.email` egyedi a Core-on
  belül (de létező e-mail nem hiba — lásd 3.3 workflow). Maxhossz: a Core
  `User.email` mérete (`00_domain_model.md` 3.1).
- `displayName` — kötelező; nem üres/whitespace; max 200 karakter
  (`User.displayName`, `00_domain_model.md` 3.1).
- `roles` — kötelező; legalább egy elem (SD-38); minden elem érvényes
  `TenantRole`; duplikátummentes.

**`User` update (`PUT`):**

- `displayName` — mint fent.
- `email` — nem módosítható (3.4); ha a kérés mégis eltérő `email`-t küld,
  `400`/`422`.
- `status` — nem írható a `PUT`-tal (3.4).

**Szerepkör-szerkesztés business-szabályai (a `PUT` része):**

- A felhasználónak a szerkesztés után legalább egy `UserTenantRole`-ja kell,
  hogy maradjon ezen a tenanton (SD-38).
- A szerkesztés nem hozhatja létre az „utolsó `manager` nélküli tenant"
  állapotot (SD-38) — ha a szerkesztés az utolsó `manager`-ről venné el a
  `manager` szerepkört, `422` `reason`-kóddal.
- A hívó nem veheti el a saját `manager` szerepkörét, ha azzal magát zárná ki
  — `422`.
- Duplikált `(userId, tenantId, role)` — a felület UI-szinten megakadályozza
  (5.4); ha mégis átjut, `409`.

**`AcceptInvitationRequest`:**

- `token` — kötelező; a beváltás-workflow (3.5) ellenőrzi az érvényességet
  (létezik, `Pending`, nem lejárt).

> **Multi-tenancy érvényesítés.** A `User`/`UserTenantRole`/`UserInvitation` a
> Core DB-ben él; a tenant-szűrést a `UserManagementController` a `Tenant`
> headerből végzi (SD-36) — nem a tenant-resolution middleware, mert az a
> Tenant DbContext-re vonatkozik. Minden végpont az aktív tenantra korlátozza
> a hatókörét: a vezető idegen tenant `User`-ét nem listázza (`404` az
> `id`-ra), nem deaktiválja, nem hív meg rá szerepkört. A cross-tenant
> kísérlet naplózott (`01_kozos_mintak.md` 7.3 mintája).

### 3.7 Workflow- és business-szabályok összefoglaló

A feature három üzleti invariánst tart fenn, mindhárom az SD-38-ból:

1. **Minden felhasználónak legalább egy szerepköre van** az aktív tenanton —
   a felvétel és a szerkesztés is kikényszeríti.
2. **A tenant sosem marad vezető nélkül** — az utolsó aktív `manager`
   szerepkör nem vonható vissza, az utolsó aktív vezető nem deaktiválható.
3. **A vezető nem zárhatja ki önmagát** — sem a saját `manager` szerepkörének
   visszavonásával (ha ő az utolsó), sem a saját fiókja deaktiválásával.

Az invariánsokat a szerver mondja ki (`422` + `reason`-kód); a kliens
proaktívan is megakadályozhatja a nyilvánvaló eseteket (5.4), de az igazság
forrása a szerver.

### 3.8 Jogosultság — `authorization.json`

A felhasználó-kezelő végpontok jogosultsága a `05_jogosultsagok_v2.md` 2.8
szerint **`manager`-only**. Az `authorization.json` bejegyzések (a route-prefix
→ szerepkör-minta a `01_kozos_mintak.md` 3.3 és 8. szakasza szerint):

| Route | Method | Engedélyezett szerepkör |
|---|---|---|
| `/v1/users`, `/v1/users/{id}` | `GET` | `tenant_manager` |
| `/v1/users/invite` | `POST` | `tenant_manager` |
| `/v1/users/{id}` | `PUT` | `tenant_manager` |
| `/v1/users/{id}/deactivate`, `/reactivate` | `POST` | `tenant_manager` |
| `/v1/users/{id}/invitations/resend` | `POST` | `tenant_manager` |
| `/v1/users/invitations/accept` | `POST` | **nincs szerepkör-megkötés** — token-alapú; explicit publikus jelölés |

> A `tenant_manager` prefix a `01_kozos_mintak.md` 3.3 mintája szerint
> kiegészül a `Tenant` header kódjával futásidőben (`tenant_manager_almadi`).
> Az `accept`-végpont a kivétel: a meghívott felhasználónak még nincs JWT-je,
> ezért ez a route autentikáció nélkül elérhető, kizárólag a token védi. Ezt
> az `authorization.json`-ban explicit jelölni kell — különben a default
> „auth kell" szabály alá esne.

### 3.9 Audit és naplózás

A `User`, `UserTenantRole`, `UserInvitation` mind `AuditableEntity` — a
`CreatedAt/By`, `UpdatedAt/By` automatikus (`01_kozos_mintak.md` 7.1). Ez fedi
a fő audit-igényt: ki és mikor hívott meg egy felhasználót
(`UserInvitation.createdBy`), ki rendelt szerepkört (`UserTenantRole.createdBy`,
az SD-20 indoka), ki deaktivált (`User.updatedBy` + a `status`-váltás).

> **Megtartott hiány — felhasználó-szintű esemény-napló.** A `Ticket`-nek van
> `ActivityLog`-ja; a `User`-nek nincs ilyen dedikált esemény-naplója. A
> pilotra ez tudatos — az `AuditableEntity` „ki/mikor utoljára" szintje elég,
> és a `05_jogosultsagok` / `50_konfig` funkcionálisan nem kért felhasználó-
> audit-naplót. Ha később kell „ki mikor vonta vissza X szerepkörét" típusú
> visszakereshetőség, az egy `UserAuditLog` iteráció (8.4).

---

## 4. Admin felület

### 4.1 Érintett képernyők

A `90_sitemap_v3.md` 2.6 szerint a Beállítások lenyíló almenü; harmadik
aloldala a Felhasználók.

| Nézet | URL | Szerep |
|---|---|---|
| Felhasználó-lista | `/beallitasok/felhasznalok` | A tenant felhasználóinak listázó-oldala |
| Felhasználó-adatlap | `/beallitasok/felhasznalok/details/<id>` | Egy felhasználó megtekintése; innen indul a szerkesztés/akciók |

A meghívó beváltó-oldala (az `accept`-flow) nem itt él — autentikáció nélküli,
valószínűleg Zitadel-oldali képernyő (lásd 3.5 megtartott hiány).

> **Eltérés a `50_konfig` „egyszerűbb lista" megjegyzésétől — nincs.** A
> `50_konfig` 3.2 jelezte, hogy a Beállítások listái „egyszerűbb listák". A
> felhasználó-listára a teljes `TableStateConfig`-mintát alkalmazzuk — kevés
> sorral is jól működik, és a szerepkör/állapot szerinti szűrés valódi érték.
> Ez nem ütközés: a `50_konfig` 7.3 azt mondta, a Beállítások „nem igényel új
> UI-komponenst" — a `TableStateConfig` épp meglévő komponens. Az „egyszerűbb"
> itt azt jelenti, hogy nincs új komponens-igény, nem azt, hogy a standard
> listázót le kell butítani.

### 4.2 Felhasználó-lista — `TableStateConfig` vázlat

A lista a standard `TableStateConfig`-mintára épül (pagináció, fejléc-szűrés,
rendezés, oszlop-láthatóság, URL query param — ezeket a minta adja).

**Oszlopok:**

| Oszlop | Forrás | Szűrhető | Rendezhető | Megjegyzés |
|---|---|---|---|---|
| Név | `UserListDto.displayName` | igen (szöveg) | igen | — |
| E-mail | `UserListDto.email` | igen (szöveg) | igen | — |
| Szerepkörök | `UserListDto.roles` (aggregált, 3.2) | igen (szerepkör-választó) | nem | Több szerepkör chip-szerűen; a választó a `TenantRole` enum |
| Állapot | `UserListDto.status` | igen (állapot-választó) | igen | `Aktív` / `Meghívva` / `Letiltva` — a `UserStatus` magyar leképezése |
| Meghívó | `UserListDto` meghívó-aggregátum (3.2) | nem | nem | Csak `Invited` sornál: „Érvényes <dátum>-ig" vagy „Lejárt" badge |
| Létrehozva | `User.createdAt` (audit) | nem | igen | Tenant-időzónában (SD-10) |

**Szűrők:** a fejléc-szűrők a fenti táblázat „Szűrhető" oszlopa szerint. A
standard minta üres szűrővel indul.

**Sor-akciók:**

| Akció | Mikor látható | Mit hív |
|---|---|---|
| Megtekintés / megnyitás | mindig | navigál az adatlapra |
| Meghívó újraküldése | csak `Invited` soron | `POST /v1/users/{id}/invitations/resend` |
| Deaktiválás | csak `Active` / `Invited` soron | `POST /v1/users/{id}/deactivate` |
| Reaktiválás | csak `Disabled` soron | `POST /v1/users/{id}/reactivate` |

**Lista-szintű akció:** „Felhasználó meghívása" gomb — megnyitja a
meghívó-űrlapot (4.4).

> **Eltérés a mintától — bulk-műveletek nincsenek.** A standard
> `TableStateConfig` bulk select/action-t is adna. A felhasználó-listán a
> pilotra nincs bulk-művelet — nincs értelmes tömeges felhasználó-akció.
> Konzisztens a bejelentés-feature pilot-döntésével és a K-016 alapszint-elvével.
> *(Iterációba, 8.2.)*

### 4.3 Felhasználó-adatlap — mezősablon

Az adatlap a megtekintő nézet; a szerkesztés innen indul (a meglévő admin-minta
„adatlap → szerkesztés" mintája szerint). A `[validationForm]`-minta adja a
szerver-oldali field error megjelenítését.

**Alapadatok szekció.** A `displayName` egyszerű, kötelező szöveges mező —
rövid forma: Név, `displayName`, szöveg, kötelező, max 200, i18n kulcs
`users.field.displayName`, szerkeszthető.

Az `email` nem-triviális (a szerkeszthetősége állapotfüggő), teljes mezősablon:

```
- Mező megjelenő neve: E-mail
- i18n kulcs: users.field.email
- Technikai név (API, camelCase): email
- Adatmodell-megfeleltetés: User.email
- Típus és méret: string, max — a Core User.email mérete
- Kötelezőség: kötelező (meghíváskor)
- Validáció: e-mail formátum; a 409-et (már tag) a szerver adja, a felület
  hibaként jeleníti meg
- Default érték: nincs
- Láthatóság: mindig
- Szerkeszthetőség: CSAK a meghívó-űrlapon (új felhasználónál). Aktivált
  felhasználó adatlapján OLVASÁSRA — az email nem módosítható (3.4)
- Interakció: a meghívó-űrlapon mentéskor szerver-oldali duplikátum-ellenőrzés
- Tenant-szinten konfigurálható: nem
- Eredet: 00_domain_model.md 3.1 (User.email)
- Megjegyzés: a Zitadel-bejelentkezés azonosítója — ezért nem szerkeszthető
  aktiválás után
```

**Szerepkörök szekció** — a feature érdemi UI-eleme, teljes mezősablon:

```
- Mező megjelenő neve: Szerepkörök
- i18n kulcs: users.field.roles
- Technikai név (API, camelCase): roles
- Adatmodell-megfeleltetés: UserTenantRole sorok (userId + aktív tenantId)
- Típus és méret: TenantRole[] — a négy enum-értékből
- Kötelezőség: kötelező — legalább egy (SD-38)
- Validáció (kliens): legalább egy bejelölt elem; a kliens nem enged nulla
  szerepkörrel menteni
- Validáció (FluentValidation szerver): a 3.6 szabálykészlet — legalább egy
  elem, az utolsó-manager-korlát, a saját-kizárás-tiltás
- Default érték: nincs (meghíváskor a vezető tölti ki)
- Láthatóság: mindig
- Szerkeszthetőség: a manager szerkesztheti; a SD-38 korlátok a mentéskor
  szerver-oldalon érvényesülnek
- Interakció: négy jelölőnégyzet (Diszpécser / Vezető / Tartalomkezelő /
  Terepi dolgozó); bejelölés/levétel a UserTenantRole-sort ad/vesz
- Tenant-szinten konfigurálható: nem (a négy szerepkör termék-szintű)
- Eredet: 05_jogosultsagok_v2.md 1. (a négy szerepkör), K-025
- Megjegyzés: UI-modell — lásd 4.4
```

**Csoporttagság szekció** — olvasásra: a felhasználó csoporttagsága, csak
olvasásra. A `50_konfig` 6.2 döntése: a csoporttagság a csoport-adatlapról
szerkeszthető, a felhasználó-adatlapon megjelenik, de nem szerkeszthető. Ha
üres, „Nincs csoporttagság".

**Állapot- és meghívó-szekció** — olvasásra, akció-gombokkal: Állapot
(`User.status` magyar leképezése); Meghívó állapota (csak `Invited`
felhasználónál — a `Pending` `UserInvitation` lejárati dátuma vagy „Lejárt",
mellette az „Újraküldés" akció); Audit (`Létrehozva` / `Módosítva` a standard
`AuditableEntity`-minta szerint, tenant-időzónában).

**Akció-gombok az adatlapon:** „Szerkesztés"; állapottól függően
„Deaktiválás" / „Reaktiválás"; `Invited`-nél „Meghívó újraküldése".

### 4.4 A szerepkör-hozzárendelő UI-modell

**Modell:** négy jelölőnégyzet (a négy `TenantRole`), a felhasználó-szerkesztő
űrlap része. A mentés a felhasználó-adatlap egészével együtt, **egy
`PUT /v1/users/{id}` hívásban** megy — a `roles` tömb a kérés része, és a
szerver a `UserTenantRole`-sorokat ehhez igazítja (új szerepkör → sor
beszúrása; levett szerepkör → sor törlése). Indok: a felhasználó ritkán
szerkesztett, kevés mezős entitás; a négy szerepkör elfér egy űrlapon a névvel
együtt, és egy mentés egyszerűbb a vezetőnek, mint külön „szerepkör" panel.
Nincs külön szerepkör-végpont.

> **Eltérés a mintától — a `roles` nem egy `User`-mező, mégis a `PUT`-ban
> utazik.** A `UserTenantRole` külön entitás (SD-20), de a felhasználó-
> szerkesztő űrlap a `roles` tömböt a `User`-update kérés részeként küldi, és
> a `UserManagementController` egy `BeforeSaveAsync`/`AfterSaveAsync`-szerű
> lépésben szinkronizálja a `UserTenantRole`-sorokat. Ez tudatos eltérés a
> tiszta entitás-per-végpont mintától — a UX (egy űrlap, egy mentés) indokolja.

**A korlátok megjelenése a felületen:** ha a vezető a SD-38-ba ütköző
szerepkör-kombinációt próbál menteni, a szerver `422`-t ad `reason`-kóddal, és
a `[validationForm]`-minta a `roles` mezőhöz rendelt hibaként jeleníti meg
magyarul, az i18n-kulcsból (4.6). A kliens emellett proaktívan is
megakadályozhatja a nyilvánvaló esetet (a vezető a saját adatlapján a `Vezető`
jelölőnégyzetet nem tudja levenni, ha ő az utolsó) — de az igazságot a szerver
mondja ki.

### 4.5 Üres / betöltési / hibaállapot

| Állapot | Viselkedés |
|---|---|
| Üres lista | „Még nincs felhasználó" — a gyakorlatban ritka (a tenant legalább egy vezetővel indul); a standard üres-állapotot a minta adja |
| Betöltés | A standard `TableStateConfig` skeleton/spinner mintája |
| Lista-hiba | A standard lista-hibaállapot (újrapróbálkozás) |
| Adatlap — `404` | Idegen tenant `id`-ja vagy nem létező felhasználó (3.2) — a standard „nem található" nézet |
| Akció-hiba (`409`/`422`/`410`) | A `reason`-kódhoz tartozó magyar üzenet (4.6) — toast vagy mezőszintű hiba a `[validationForm]`-minta szerint |

### 4.6 i18n kulcsok

Minden szöveg a `hu.json`-ba, kulcs-alapon (`01_kozos_mintak.md` i18n-elvek;
a felület tegez). A szerver kulcsot/kódot ad, nem kész szöveget — a
`409`/`422`/`410` hibatest a `reason`-kódot hordozza, a magyar mondatot az
admin felület i18n-rétege fogalmazza (`01_kozos_mintak.md` 5.5, `CLAUDE.md` 4.).

**Mező / állapot / gomb kulcsok (kivonat):** `users.field.displayName`,
`users.field.email`, `users.field.roles`, `users.field.groups`,
`users.status.active` / `.invited` / `.disabled`, `users.role.dispatcher` /
`.manager` / `.contentManager` / `.fieldWorker`, `users.action.invite` /
`.resend` / `.deactivate` / `.reactivate` / `.edit`,
`users.invitation.validUntil`, `users.invitation.expired`.

**Hibaüzenet- és konfliktus-kulcsok (a 3. szakasz hibakódjaihoz — kötelező
rész):**

| `reason`-kód / eset | i18n kulcs | Tartalom (magyar, tegező) |
|---|---|---|
| `409` — már tagja a tenantnak | `users.error.alreadyMember` | „Ez a felhasználó már tagja a városnak." |
| `422` — utolsó vezető deaktiválása | `users.error.lastManager` | „Nem deaktiválhatod az utolsó vezetőt — előbb nevezz ki másikat." |
| `422` — utolsó manager szerepkör elvétele | `users.error.lastManagerRole` | „Legalább egy vezetőnek maradnia kell — ezt a szerepkört nem veheted el." |
| `422` — saját maga deaktiválása | `users.error.cannotDeactivateSelf` | „A saját fiókodat nem deaktiválhatod." |
| `422` — szerepkör nélküli mentés | `users.error.roleRequired` | „Legalább egy szerepkört ki kell választanod." |
| `422` — reaktiválás, de új meghívó kell | `users.error.reactivateNeedsInvite` | „Ez a felhasználó még nem aktiválta a fiókját — küldj neki új meghívót." |
| `422` — újraküldés nem `Invited` felhasználóra | `users.error.resendNotInvited` | „Ennek a felhasználónak már aktív a fiókja — nincs mit újraküldeni." |
| `410` — lejárt meghívó (beváltáskor) | `users.error.invitationExpired` | „Ez a meghívó lejárt. Kérj újat a városod ügyintézőjétől." |

A felhasználó-feature-nek nincs esemény-naplója (3.9), így esemény-sablon-kulcs
itt nem kell.

---

## 5. Polgári mobilapp adatigénye

**Nem releváns, mert** ez a feature kizárólag a városgazdálkodás belső
felhasználóit kezeli. A `00_terminologia.md` élesen elhatárolja: a `User` /
`UserTenantRole` / `TenantUser` a négy belső szerepkör világa; a polgár nem
`User` ebben az értelemben — ő a polgári mobilapp felhasználója, a
`Ticket.reporter`, és az ő autentikációja és entitása külön kérdés (`NY-2`,
`02_kerdeslista` 4.).

- A polgári mobilapp egyetlen API-ját sem hívja ennek a feature-nek a
  végpontjai közül — mind `tenant_manager`-védett, admin-oldali végpont.
- A `POST /v1/users/invitations/accept` az egyetlen autentikáció nélküli
  végpont — de azt belső felhasználó (meghívott diszpécser/vezető/stb.) váltja
  be, nem a polgár, és nem a polgári mobilappból.
- A polgári mobilappnak nincs felhasználó-kezelő képernyője. A polgári app
  „Profilom" menüpontja a polgár saját fiókja — külön entitás, külön feature.

Adat-szintű nyitott kérdés: nincs — a feature nem érint polgári Flutter-
képernyőt, így nincs visszaolvasandó adatigény és nincs hiányos képernyő.

---

## 6. Acceptance criteria

### AC-1 — Meghívás-indítás

**AC-1.1** — *Given* egy vezető a saját tenantján, *When* meghív egy
felhasználót érvényes `email`, `displayName` és legalább egy `roles` elemmel,
*Then* létrejön egy `User` `status=Invited` állapotban, létrejönnek a
`UserTenantRole`-sorok a `roles` minden eleméhez, létrejön egy `Pending`
`UserInvitation` `expiresAt = createdAt + 7 nap` értékkel, és a válasz
`201 Created`.

**AC-1.2** — *Given* egy meghívás sikeresen lement, *When* a tranzakció
lezárul, *Then* a meghívott `email`-címére meghívó-e-mail indul a nyers
tokennel, és a `UserInvitation.tokenHash` a token hash-elt alakját tárolja (a
nyers token a DB-ben sehol nem szerepel).

**AC-1.3** — *Given* egy meghívás, ahol az e-mail-küldés meghiúsul, *When* a
kérés feldolgozódik, *Then* a `User`, a `UserTenantRole`-sorok és a
`UserInvitation` létrejönnek (a tranzakció nem gördül vissza), és a vezető a
felületről újraküldheti a meghívót.

**AC-1.4** — *Given* egy `email`, amely még nem létezik a Core `User`-ben,
*When* a vezető meghívja, *Then* új `User` jön létre, és a válasz
`201 Created`.

**AC-1.5** — *Given* egy `email`, amely már egy aktív Core `User`-é, de annak
nincs `UserTenantRole`-ja az aktív tenanton, *When* a vezető meghívja, *Then*
nem jön létre új `User`, csak új `UserTenantRole`-sor(ok) az aktív tenantra,
`UserInvitation` nem jön létre, és a válasz `200 OK`.

**AC-1.6** — *Given* egy `email`, amely már egy Core `User`-é, és annak van
`UserTenantRole`-ja az aktív tenanton, *When* a vezető meghívja, *Then* a
válasz `409`, `reason`-kóddal, amely a `users.error.alreadyMember` kulcsra
képződik le.

**AC-1.7** — *Given* egy meghívási kérés, amelynek `roles` tömbje üres, *When*
a kérés feldolgozódik, *Then* a válasz `400`, `fieldErrors` jelzéssel a `roles`
mezőn.

**AC-1.8** — *Given* egy meghívási kérés üres vagy csak whitespace
`displayName`-mel, vagy érvénytelen `email`-formátummal, *When* a kérés
feldolgozódik, *Then* a válasz `400`, `fieldErrors` jelzéssel az érintett
mezőn.

**AC-1.9** — *Given* egy felhasználó, akinek nincs `manager` szerepköre az
aktív tenanton, *When* a `POST /v1/users/invite` végpontot hívja, *Then* a
válasz `403`.

### AC-2 — Meghívó beváltása

**AC-2.1** — *Given* egy `Pending`, nem lejárt `UserInvitation` és a hozzá
tartozó nyers token, *When* a `POST /v1/users/invitations/accept` a tokennel
meghívódik, *Then* a Zitadel-fiók létrejön, a `User.externalAuthId`
kitöltődik, a `User.status` `Active`-ra vált, a `UserInvitation.status`
`Accepted`-re vált és a `consumedAt` kitöltődik.

**AC-2.2** — *Given* egy `UserInvitation`, amelynek `expiresAt`-je a múltban
van, *When* a beváltást a tokennel megkísérlik, *Then* a `UserInvitation.status`
`Expired`-re frissül, és a válasz `410`, `reason`-kóddal, amely a
`users.error.invitationExpired` kulcsra képződik le.

**AC-2.3** — *Given* egy érvénytelen vagy nem létező token, *When* a beváltást
megkísérlik, *Then* a válasz `410` (vagy `404`), és nem jön létre Zitadel-fiók.

**AC-2.4** — *Given* egy már beváltott (`Accepted`) `UserInvitation`, *When*
ugyanazzal a tokennel újra beváltást kísérelnek meg, *Then* a beváltás nem
hajtódik végre újra (a token egyszer használatos), és a válasz hibakóddal tér
vissza.

**AC-2.5** — *Given* egy `Pending`, érvényes meghívó, *When* a beváltás során
a Zitadel-oldali fióklétrehozás meghiúsul, *Then* a tranzakció visszagördül: a
`User` `Invited` marad, a `UserInvitation` `Pending` marad, és a felhasználó a
linkkel újrapróbálhatja.

**AC-2.6** — *Given* a `POST /v1/users/invitations/accept` végpont, *When* JWT
nélküli (autentikálatlan) kérés érkezik rá érvényes tokennel, *Then* a kérés
feldolgozódik (a végpont nem szerepkör-védett; a token az azonosítás).

### AC-3 — Meghívó újraküldése

**AC-3.1** — *Given* egy `Invited` állapotú felhasználó egy `Pending`
meghívóval, *When* a vezető a meghívót újraküldi, *Then* a korábbi
`UserInvitation` `Revoked`-ra vált, új `UserInvitation` jön létre (`Pending`,
friss `expiresAt = now + 7 nap`, új token), és új meghívó-e-mail indul.

**AC-3.2** — *Given* egy újraküldés után a felhasználó a régi (most már
`Revoked`) tokennel kísérli a beváltást, *When* a beváltás feldolgozódik,
*Then* a beváltás elutasításra kerül, és csak az új token érvényes.

**AC-3.3** — *Given* egy `Active` állapotú felhasználó, *When* a vezető rá
meghívó-újraküldést kísérel meg, *Then* a válasz `422`, `reason`-kóddal, amely
a `users.error.resendNotInvited` kulcsra képződik le.

### AC-4 — Deaktiválás

**AC-4.1** — *Given* egy `Active` felhasználó, aki nem az utolsó aktív
`manager` és nem a hívó maga, *When* a vezető deaktiválja, *Then* a
`User.status` `Disabled`-ra vált, és a `TenantUser`-projekció `status`-a
frissül.

**AC-4.2** — *Given* egy `Invited` felhasználó, *When* a vezető deaktiválja,
*Then* a `User.status` `Disabled`-ra vált, és a hozzá tartozó `Pending`
`UserInvitation` `Revoked`-ra vált.

**AC-4.3** — *Given* egy tenant, ahol pontosan egy aktív `manager` van, *When*
a vezető ezt az utolsó aktív vezetőt deaktiválni próbálja, *Then* a válasz
`422`, `reason`-kóddal, amely a `users.error.lastManager` kulcsra képződik le,
és a `User.status` változatlan marad.

**AC-4.4** — *Given* egy bejelentkezett vezető, *When* a saját fiókját
deaktiválni próbálja, *Then* a válasz `422`, `reason`-kóddal, amely a
`users.error.cannotDeactivateSelf` kulcsra képződik le.

**AC-4.5** — *Given* egy felhasználó `manager` szerepkör nélkül, *When* a
`POST /v1/users/{id}/deactivate` végpontot hívja, *Then* a válasz `403`.

### AC-5 — Reaktiválás

**AC-5.1** — *Given* egy `Disabled` felhasználó, akinek van `externalAuthId`-ja
(volt már aktív), *When* a vezető reaktiválja, *Then* a `User.status`
`Active`-ra vált, és nem keletkezik új `UserInvitation`.

**AC-5.2** — *Given* egy `Disabled` felhasználó, akinek nincs
`externalAuthId`-ja (sosem aktivált, `Invited`-ből lett `Disabled`), *When* a
vezető reaktiválást kísérel meg, *Then* a válasz `422`, `reason`-kóddal, amely
a `users.error.reactivateNeedsInvite` kulcsra képződik le.

### AC-6 — Szerepkör-szerkesztés és a korlátok

**AC-6.1** — *Given* egy felhasználó, *When* a vezető a felhasználó-szerkesztő
űrlapon új szerepkört jelöl be és ment, *Then* a bejelölt szerepkörhöz új
`UserTenantRole`-sor jön létre az aktív tenantra; *When* egy szerepkört levesz
és ment, *Then* a megfelelő `UserTenantRole`-sor törlődik.

**AC-6.2** — *Given* egy felhasználó-szerkesztés, amely után a felhasználónak
nulla `UserTenantRole`-ja maradna az aktív tenanton, *When* a mentést
megkísérlik, *Then* a válasz `422`, `reason`-kóddal, amely a
`users.error.roleRequired` kulcsra képződik le.

**AC-6.3** — *Given* egy tenant, ahol pontosan egy felhasználónak van
`manager` szerepköre, *When* a szerkesztés ezt a `manager` szerepkört venné el
róla, *Then* a válasz `422`, `reason`-kóddal, amely a
`users.error.lastManagerRole` kulcsra képződik le.

**AC-6.4** — *Given* egy bejelentkezett vezető, aki az utolsó `manager`, *When*
a saját adatlapján a `Vezető` szerepkört próbálja levenni, *Then* a mentés
`422`-vel elutasításra kerül (a kliens emellett proaktívan is megakadályozhatja
a jelölőnégyzet levételét, de a szerver mondja ki az igazságot).

**AC-6.5** — *Given* egy felhasználó, akinek már van egy adott
`(userId, tenantId, role)` szerepköre, *When* ugyanaz a szerepkör-hozzárendelés
ismételten beérkezik a szerverre, *Then* a válasz `409` (a hármas egyedi).

**AC-6.6** — *Given* egy felhasználó-szerkesztő kérés, amely az `email` mezőt
az eredetitől eltérő értékre módosítaná, *When* a mentést megkísérlik, *Then*
az `email` nem módosul (a kérés `400`/`422`-vel elutasításra kerül vagy az
`email`-t figyelmen kívül hagyja — a végpont determinisztikusan az egyiket
teszi).

### AC-7 — Listázás, lekérdezés, tenant-szigetelés

**AC-7.1** — *Given* két tenant, mindegyikben saját felhasználókkal, *When* az
egyik tenant vezetője a `GET /v1/users` végpontot hívja a saját `Tenant`
headerével, *Then* a válasz kizárólag azokat a `User`-eket tartalmazza,
akiknek van `UserTenantRole`-juk az aktív tenanton.

**AC-7.2** — *Given* egy `User`, amelynek nincs `UserTenantRole`-ja az aktív
tenanton (másik tenant felhasználója), *When* a vezető a `GET /v1/users/{id}`
végponton lekéri ezt az `id`-t, *Then* a válasz `404`.

**AC-7.3** — *Given* egy idegen tenant `User`-ének `id`-ja, *When* a vezető rá
deaktiválást, reaktiválást vagy szerkesztést kísérel meg, *Then* a válasz
`404`, és a művelet nem hajtódik végre.

**AC-7.4** — *Given* egy `Invited` felhasználó egy `Pending`, nem lejárt
meghívóval, *When* a vezető a felhasználó-listát lekéri, *Then* a sor `Állapot`
oszlopa `Meghívva`, és a `Meghívó` oszlop a meghívó érvényességi dátumát
mutatja.

**AC-7.5** — *Given* egy `Invited` felhasználó, akinek a meghívója lejárt,
*When* a vezető a felhasználó-listát lekéri, *Then* a sor `Meghívó` oszlopa
„Lejárt" jelzést mutat.

**AC-7.6** — *Given* egy felhasználó több szerepkörrel az aktív tenanton,
*When* a vezető a felhasználó-listát lekéri, *Then* a `Szerepkörök` oszlop a
felhasználó összes szerepkörét mutatja az aktív tenanton.

### AC-8 — Csoporttagság megjelenítése

**AC-8.1** — *Given* egy felhasználó, aki tagja egy vagy több `Group`-nak,
*When* a vezető megnyitja a felhasználó-adatlapot, *Then* a Csoportok szekció a
felhasználó csoporttagságait csak olvasásra mutatja (szerkesztő-kontroll
nélkül).

**AC-8.2** — *Given* egy felhasználó, aki egyetlen csoportnak sem tagja,
*When* a vezető megnyitja a felhasználó-adatlapot, *Then* a Csoportok szekció a
„nincs csoporttagság" üres állapotot mutatja.

### AC-9 — Jogosultság-érvényesítés

**AC-9.1** — *Given* az `authorization.json` felhasználó-kezelő bejegyzései,
*When* a route-szabályok kiértékelődnek, *Then* minden felhasználó-kezelő
végpont (`GET /v1/users`, `invite`, `PUT`, `deactivate`, `reactivate`,
`resend`) kizárólag a `tenant_manager` (a `Tenant` header kódjával kiegészített)
szerepkört engedi.

**AC-9.2** — *Given* a `POST /v1/users/invitations/accept` route, *When* az
`authorization.json` kiértékeli, *Then* a route autentikáció nélkül elérhető
(explicit publikus jelölés).

**AC-9.3** — *Given* egy felhasználó `tenant_dispatcher` szerepkörrel, *When*
bármely `tenant_manager`-védett felhasználó-kezelő végpontot hív, *Then* a
válasz `403`.

**AC-9.4** — *Given* egy érvényes JWT egy adott tenant szerepkörével, *When* a
kérés egy másik tenant `Tenant` headerével érkezik, *Then* a kérés a
`01_kozos_mintak.md` hibakód-kerete szerint elutasításra kerül (`403`/`404`),
és a cross-tenant kísérlet naplózódik.

**AC-9.5** — *Given* a `vezető ⊇ diszpécser` reláció, *When* egy
`tenant_manager` szerepkörű felhasználó egy diszpécser-akció route-ját hívja,
*Then* a kérés engedélyezett (az `authorization.json` a diszpécser-route-okon
a `manager` prefixet is felsorolja).

### AC-10 — Landing-redirect

**AC-10.1** — *Given* egy felhasználó egyetlen szerepkörrel, *When* a `/`
URL-re lép, *Then* a `05_jogosultsagok_v2.md` 4. tábla szerinti landing-nézetre
irányítódik (diszpécser → `/bejelentesek`, vezető → `/fooldal`, tartalomkezelő
→ `/tartalom/hirek`, terepi → `/bejelentesek`).

**AC-10.2** — *Given* egy felhasználó több szerepkörrel, *When* a `/` URL-re
lép, *Then* a „magasabb" szerepköréhez tartozó landingre irányítódik (a `05`
4. szabálya szerint: vezető jelenléte → `/fooldal`).

> **AC-10.3 — feltételhez kötött, nincs lezárva.** *Given* egy felhasználó,
> akinek nulla `UserTenantRole`-ja van az aktív tenanton, *When* a `/` URL-re
> lép, *Then* … — ez a kritérium nyitva marad. A SD-38 miatt ez az állapot a
> normál működésben nem áll elő (a felhasználó nem jöhet létre és nem
> szerkeszthető nulla szerepkörre). A kritériumot mégis jelezzük, mert egy
> adat-inkonzisztencia (pl. közvetlen DB-beavatkozás) elvileg előállíthatja —
> de mivel a feature ezt az állapotot tiltja, nem definiálunk rá kötelező
> viselkedést; ha a fejlesztő mégis védőhálót akar (pl. „nincs jogosultságod"
> oldal), az fejlesztői döntés (8.3).

---

## 7. Keresztmetszeti

- **i18n** — minden szöveg kulcs-alapú (`hu.json`), a 4.6 szerint; a szerver
  kulcsot/kódot ad. **Érdemi.**
- **Időzóna** — a felhasználó-lista `Létrehozva` oszlopa és a meghívó lejárati
  dátumának megjelenítése tenant-időzónában (`01_kozos_mintak.md` 5.2, SD-10);
  a `UserInvitation.expiresAt` UTC-ben tárolva. **Érdemi.**
- **Biztonság** — a meghívó-token hash-elve tárolódik, sosem nyersen
  (2.2, AC-1.2); a token egyszer használatos és lejáratos; az `accept`-végpont
  autentikáció nélküli, kizárólag a token védi; a token-érvénytelenítés
  szerepkör-visszavonáskor eventual consistency (TD-B3 — a token a lejártáig
  hordozhatja a régi szerepköröket; aktív invalidálás iterációba, 8.2).
  **Érdemi.**
- **Teljesítmény** — a felhasználó-kör pilot-volumene kicsi (tipikusan
  egy-két számjegyű felhasználószám tenantonként); a `User`-lista
  offset-lapozott, nincs külön optimalizációs igény. **Megjegyzés-szinten
  releváns.**

---

## 8. Lezárás

### 8.1 Első kiadás (MVP) — a piloton (T0)

- **Jogosultság-modell** — a `TenantRole` enum négy értékének route-szabályokká
  fordítása az `authorization.json`-ban; a `vezető ⊇ diszpécser ÉS
  tartalomkezelő` reláció felsorolással; a kétrétegű érvényesítés platform-
  mintája a `01_kozos_mintak.md` 8. szakaszában; a tenant-szigetelés
  (`403`/`404` idegen tenantra, cross-tenant kísérlet naplózva).
- **Szerepkör-érzékeny landing-redirect** — a `/` URL a `05_jogosultsagok_v2.md`
  4. tábla szerint irányít.
- **Felhasználó-lista** — `/beallitasok/felhasznalok`, `TableStateConfig`
  szerint; szűrés szerepkörre és állapotra; sor-akciók (újraküldés,
  deaktiválás, reaktiválás).
- **Felhasználó-adatlap** — `/beallitasok/felhasznalok/details/<id>`,
  megtekintés + szerkesztés; négy jelölőnégyzetes szerepkör-hozzárendelés; a
  csoporttagság olvasásra.
- **Meghívó-flow** — meghívás-indítás, 7 napos lejáratú token, beváltás,
  újraküldés; a `UserInvitation` entitás és a `User`-állapotgép.
- **Deaktiválás / reaktiválás** — a `User`-állapotgép `Active ⇄ Disabled`
  átmenetei, az SD-38 korlátaival.
- **A `TenantUser`-projekció `status`-mezeje** (SD-37).

### 8.2 Következő iterációk (listázva, nem specifikálva)

- E-mail-cím módosítása aktivált felhasználón — a pilotra kizárva (3.4),
  Zitadel-oldali identitás-átkötést igényel.
- Aktív token-invalidálás szerepkör-visszavonáskor — a pilotra eventual
  consistency; ha egy nagyobb tenant biztonsági profilja indokolja, iterációs
  elem.
- Felhasználó-szintű esemény-napló (`UserAuditLog`) — a pilotra az
  `AuditableEntity` szintje elég.
- Bulk-műveletek a felhasználó-listán — a pilotra nincs (4.2).
- Meghívó-lejárat háttér-job — a pilotra lazy ellenőrzés (2.2); időzített
  `Pending → Expired` job iterációba.
- Új szerepkörök (moderátor, polgármester, önálló tenant-admin — a
  `00_architektura_v4` szerint 6–12. hó); a modell úgy épült, hogy ez „egy új
  oszlop a mátrixban" — egy új `TenantRole` enum-érték és
  `authorization.json`-bejegyzések, architektúra-újratervezés nélkül.

### 8.3 Feltételezések (explicit lista)

1. A multi-tenancy modell zárt — az SD-1 érvényes; a `User`/`UserTenantRole`/
   `UserInvitation` a Core DB-ben él, a `TenantUser` a Tenant DB-ben. A
   `project_backend` CLAUDE.md frissítése (az SD-1 teendője) fejlesztői
   oldalon megtörténik.
2. A Zitadel az identity-szolgáltató — a fióklétrehozás, a jelszó-kezelés, a
   2FA Zitadel-oldali; ez a feature a Zitadelt adottságként kezeli, és csak az
   `externalAuthId`-n keresztül kötődik hozzá.
3. A `00_domain_model.md` 3. blokkja a kész alap — a `User`, `UserTenantRole`,
   `UserStatus`, `TenantRole` definíciója adott; ez a feature ezekre épít.
4. A négy szerepkör termék-szintű — a `dispatcher`/`manager`/`content_manager`/
   `field_worker` készlet a pilotra rögzített (`05` 1., SD-7).
5. A `Tenant` entitás adottság — a tenant létrehozása Urbino-admin hatáskör
   (K-024).
6. A nulla-szerepkörű felhasználó nem áll elő — az SD-38 miatt a normál
   működésben minden felhasználónak legalább egy `UserTenantRole`-ja van; az
   AC-10.3 ezért nincs lezárva. Ha egy adat-inkonzisztencia mégis előállítaná,
   a felület viselkedése fejlesztői döntés.

### 8.4 Nyitott kérdések és megtartott hiányok

**Nyitott kérdés — lezárva ezzel a feature-rel:**

| # | Kérdés | Lezárás |
|---|---|---|
| `NY-5` | A meghívott, de még nem aktivált `User` és a Zitadel-identitás kötése — mikor és hogyan tölti ki a meghívó-flow az `externalAuthId`-t | **Lezárva:** az `externalAuthId` a meghívó beváltásakor (`POST /v1/users/invitations/accept`) töltődik ki, a Zitadel-oldali fióklétrehozás eredményéből; a `User.status` ekkor vált `Invited → Active`-ra. A `00_domain_model.md` nyitott-kérdés-táblájában az `NY-5` átkerül a „Lezárt kérdések" közé, erre a feature-specre hivatkozva. |

**Megtartott hiányok (a feature tudatosan nem specifikálja — a gerincet nem
érintik):**

| Hiány | Mi hiányzik | Kinek a hatásköre |
|---|---|---|
| A meghívó beváltó-képernyőjének UX-e | A beváltás technikai lánca kész; de hogy a felhasználó mit lát beváltás közben — admin-oldali aktiváló-oldal jelszó-megadással, vagy a Zitadel saját hosted onboarding-képernyője — funkcionálisan nincs megtervezve | Fejlesztői (a Zitadel hosted-flow képességei) + funkcionális tisztázás; valószínű irány a Zitadel saját jelszó-állító képernyője |
| Felhasználó-szintű esemény-napló | A `User`-nek nincs `ActivityLog`-szerű dedikált esemény-naplója; az `AuditableEntity` „ki/mikor utoljára" szintje a pilotra elég | Iterációs elem (`UserAuditLog`); funkcionálisan nem kért |

**Nyitott kritérium:** az AC-10.3 (a nulla-szerepkörű felhasználó landingje)
tudatosan nincs lezárva — az SD-38 miatt elérhetetlen állapotról szól.

### 8.5 Visszacsorgó jelzések

| # | Jelzés | Cél-dokumentum |
|---|---|---|
| `VF-felh-1` | A `User.email` mezőnek a `00_domain_model.md` 3.1 nem ad explicit hosszkorlátot. A feature-spec sablon tiltja a kihagyott méretet. Javasolt: max 254 karakter (RFC 5321 e-mail-maximum). A `00_domain_model.md` 3.1 `User.email` sora egészítendő ki ezzel a mérettel. *(Ez a feature-spec és a `00_domain_model.md` v1.3 frissítése együtt rendezi — lásd a domain-modell verziónaplóját.)* | `00_domain_model.md` |

### 8.6 Hivatkozott dokumentumok

- **Funkcionális:** `05_jogosultsagok_v2.md`, `00_architektura_v4.md`,
  `50_konfig_v2.md`, `90_sitemap_v3.md`, `manager_felulet_atadas.md`
- **Alapdokumentumok:** `00_domain_model.md` (a `User`/`UserTenantRole`/
  `Tenant`/`TenantUser` entitások, a `UserInvitation`), `01_kozos_mintak.md`
  (multi-tenancy, szerepkör-modell, `authorization.json` 8. szakasz, hibakód-
  keret, i18n), `00_terminologia.md`, `99_donesnaplo.md`
- **Kanonikus:** `kanonikus_donek.md` — K-016, K-024, K-025, K-027, K-038
- **Meglévő minták:** `project_backend` CLAUDE.md, `project_backend_client_angular`
  CLAUDE.md (`BaseController`, `TableStateConfig`, `[validationForm]`,
  `authorization.json`)
- **`CLAUDE.md`** (`urbino-docs`) — a hézag-kezelési konvenciók

---

## Verziónapló

- **v1.0 (2026.05.19)** — Első kiadás. A felhasználókezelés
  (`/beallitasok/felhasznalok`) és a jogosultság-modell érvényesítésének teljes
  fejlesztői specifikációja: a `UserInvitation` új entitás, a `User`-állapotgép,
  a hét végpont (list/get, invite, update, deactivate, reactivate, accept,
  resend), a `TableStateConfig`- és mezősablon-vázlat, a szerepkör-
  hozzárendelő UI-modell, 10 acceptance-blokk. A feature hat specifikációs
  döntésre épül (SD-34 — SD-39, lásd `99_donesnaplo.md`). Az `NY-5` lezárva;
  két megtartott hiány (beváltó-képernyő UX-e, felhasználó-esemény-napló); egy
  visszacsorgó jelzés (`VF-felh-1` — a `User.email` hosszkorlátja).
