Modul Accounts
Uživatelské účty a role – registrace, přihlášení, role User/Admin, kdo smí co, první admin, správa uživatelů a audit. Data v users.db (migrace).
Na této stránce
V jedné větě: Accounts ví, kdo je přihlášený a co smí: uživatel se zaregistruje e-mailem a heslem, při přihlášení dostane zašifrovanou cookie se svými rolemi a Web podle policies rozhodne, kam ho pustí. Běží ve službě Web, data má v
users.db.
Modul vzniká postupně podle plánu Uživatelské účty a role. Hotové jsou kroky 1 (registrace, přihlášení), 2 (role, oprávnění, první admin), 3 (vlastník objednávky – zákazník vidí jen své, viz Ordering), 5 (e-maily), 6 (správa uživatelů a audit), 8 (zpevnění) a 9 (otevřený web); krok 7 zabezpečil sklad (Inventory).
Role a oprávnění
| Kde | Nepřihlášený | User (zákazník) | Admin |
|---|---|---|---|
Úvod, Dokumentace, Přihlášení, Registrace, Ochrana osobních údajů, /health |
✅ | ✅ | ✅ |
| Objednávky – seznam a detail | → přihlášení | jen své (cizí = 404) | všechny |
Objednávky – založit (stránka i POST /api/orders) |
❌ | ✅ jen s potvrzeným e-mailem (jméno a e-mail z účtu) | ❌ |
| Smazat vlastní účet | ❌ | ✅ (bez nevyřízených objednávek) | ❌ (nejdřív vrátit na usera) |
| Objednávky – zrušit (potvrzenou nebo balenou, e-shop krok 6) | ❌ | jen svou | jakoukoli |
Expedice – balit, odeslat, doručeno (/Admin/Fulfilment, e-shop krok 6), obnovit katalog |
❌ | ❌ | ✅ |
| Produkty – přidat, naskladnit, upravit, smazat | ❌ | ❌ | ✅ |
Průběh a přehrání objednávky (stránky i GET /api/journeys/…) |
❌ | ❌ | ✅ |
| Změna hesla | ❌ | ✅ | ✅ |
Uživatelé – přehled, detail, povýšit / vrátit na usera, zablokovat / odblokovat (/Admin/…) |
❌ | ❌ | ✅ (ne sám sobě) |
Pravidla jsou pojmenovaná na jednom místě – policies RequireAdmin a RequireCustomer
v AuthorizationPolicies.cs, registrované v
AccountsServiceCollectionExtensions.cs.
Celou tabulku ověřuje test AuthorizationMatrixTests
skutečnými HTTP požadavky proti celému Webu.
Jak se rozhoduje
flowchart TD
req(["Požadavek"]) --> auth["UseAuthentication<br/>cookie → User (ID, jméno, role)"]
auth --> must{"musí změnit heslo?"}
must -->|ano| change["→ /Account/ChangePassword<br/>(API: 403)"]
must -->|ne| endpoint{"stránka / endpoint má…"}
endpoint -->|"[AllowAnonymous]"| ok(["pustit"])
endpoint -->|"[Authorize(Policy)]"| policy{"splňuje policy?"}
endpoint -->|"nic"| fallback{"přihlášený?<br/>(fallback policy)"}
fallback -->|ano| ok
fallback -->|ne| login["stránka: → /Account/Login<br/>API: 401"]
policy -->|ano| ok
policy -->|"ne, nepřihlášený"| login
policy -->|"ne, přihlášený"| denied["stránka: → /Account/AccessDenied<br/>API: 403"]
- Secure by default (fallback policy): co není výslovně
[AllowAnonymous], vyžaduje přihlášení. Nová stránka je zamčená, i když na ni autor zapomene. - Stránka Objednávky má víc akcí (handlerů) s různými pravidly – Razor Pages umí
[Authorize]jen na celou stránku, proto handlery volajíIAuthorizationServicese stejnými policies a bez oprávnění vrátíForbid(). - Skrytí v UI je jen pohodlí (menu Produkty a Expedice, košík a pokladna, tlačítko Zrušit). Chrání server.
První admin
Admina jmenuje admin – kdo ale jmenuje prvního? (Problém slepice a vejce.)
- V produkci se první admin zakládá příkazem přímo v běžícím kontejneru:
Heslo se píše skrytě do terminálu – nikdy neleží v GitHubu, vsudo -u deploy docker compose exec web dotnet MiniShop.Web.dll create-admin --email <e-mail> --name "<jméno>".envani v historii příkazů. Příkaz projde, jen když žádný admin není. Kód: CreateAdminCommand.cs. - Ve vývoji ho při startu vytvoří seed z
AdminBootstrapvappsettings.Development.json(admin@minishop.local). Heslo je v repozitáři, proto má příznak „musí změnit heslo“ – dokud ho nezmění, přesměruje ho každá stránka na změnu hesla (MustChangePasswordMiddleware.cs). - Existující účet se adminem nikdy nestane. Bez potvrzení e-mailu by si kdokoli mohl adminův e-mail zaregistrovat dřív a seed by mu práva admina dal.
- Účty z kroku 1 (bez role) dostanou při startu roli User. Kód: AccountsSeeder.cs.
- Další admini vznikají povýšením v aplikaci (níže). Nový admin se nejdřív sám zaregistruje – heslo tak zná jen on.
Správa uživatelů
Menu Uživatelé (jen admin): přehled všech účtů s rolí, stavem, počtem objednávek a útratou, detail s objednávkami a historií změn. Na detailu jsou tlačítka Povýšit na admina / Vrátit na usera a Zablokovat / Odblokovat.
flowchart TD
act(["Admin klikne na akci"]) --> tx["transakce v users.db"]
tx --> actor{"je volající pořád<br/>aktivní admin? (databáze)"}
actor -->|ne| fail(["chyba – nic se nezmění"])
actor -->|ano| last{"zůstane aspoň jeden<br/>jiný aktivní admin?"}
last -->|"ne (vrátit / zablokovat posledního)"| fail
last -->|ano| self{"jde o vlastní účet?"}
self -->|"ano (vrátit / zablokovat)"| fail
self -->|ne| change["změna role User ↔ Admin<br/>nebo zámek účtu"]
change --> stamp["nový security stamp"]
stamp --> audit["záznam do AccountAuditEntries<br/>kdo, komu, co, kdy"]
audit --> commit(["commit"])
- Rozhoduje databáze, ne cookie. Admin, kterému jiný admin právě roli odebral, má možná ještě starou cookie – akce mu přesto neprojde.
- Vždy zůstane aktivní admin a nikdo si nemůže sám odebrat roli ani se zablokovat. Kontrola i změna jsou v jedné transakci: když dva admini ve stejnou chvíli odebírají jeden druhého, SQLite druhou změnu nepustí.
- Zablokování = zámek účtu Identity „navždy“ (do odblokování). Přihlášení skončí stejnou hláškou jako špatné heslo – jiná by prozradila, že účet existuje.
- Odhlášení do minuty: každá změna vymění security stamp (otisk „verze“ účtu). Web cookie každou minutu
porovná s databází (
SecurityStampValidator, interval v AccountsServiceCollectionExtensions.cs); nesedí-li, cookie zahodí. Přirovnání: přenastavený zámek – staré klíče přestanou fungovat. - Audit jen přibývá (žádná úprava ani mazání), e-maily v něm jsou kopie z doby akce.
- Objednávky dodá Ordering (
IOrderService.GetCustomerSummariesAsync, detail přesOrderAccess.Customer(id)) – moduly se neznají, spojí je až stránka ve Webu přes ID uživatele.
Kód: AccountAdministration.cs,
stránky Admin/Users a
Detail. Celá složka /Admin je navíc zamčená
konvencí AuthorizeFolder v Program.cs.
Zpevnění
Účty krok 8 přidaly několik vrstev obrany (defense in depth) – každá chytí něco, co ostatní propustí.
flowchart LR
att(["Útočník zkouší hesla"]) --> rl{"Rate limiting<br/>max. 10 odeslání formuláře<br/>za minutu z jedné IP"}
rl -->|"víc"| r429(["429 – Příliš mnoho pokusů"])
rl -->|"projde"| lock{"Zámek účtu<br/>5 špatných hesel za sebou"}
lock -->|"5×"| locked(["účet zamčený 15 minut<br/>(stejná hláška jako špatné heslo)"])
lock -->|"méně"| pwd{"heslo"}
pwd -->|"špatné"| audit["log: ID účtu, kolikátý pokus<br/>(nikdy e-mail ani heslo)"]
pwd -->|"správné"| ok(["přihlášen, počítadlo = 0"])
| Vrstva | Co dělá | Kód |
|---|---|---|
| Zámek účtu | 5 špatných hesel za sebou → 15 minut. Admin zámek vidí v přehledu („zamčený do 15:42“) a může ho zrušit. Dočasný zámek adminovi práva nebere – útočník by jinak zkoušením hesel „odstavil“ všechny adminy. | AccountsInfrastructure.cs, Login.cshtml.cs |
| Rate limiting | Formuláře přihlášení, registrace, zapomenutého a nového hesla, Poslat znovu a změny hesla: z jedné IP nejvýš 10 odeslání za minutu, pak 429. Brzdí hádání hesel, zkoušení uniklých hesel z jiných webů (credential stuffing) i zahlcení Gmailu. IP za Caddy z X-Forwarded-For. |
AccountRateLimiting.cs |
| Bezpečnostní hlavičky | CSP (skripty jen ze souborů MiniShopu – žádné inline <script>, Bootstrap lokálně), HSTS (jen HTTPS, 1 rok, bez preload), X-Frame-Options: DENY + frame-ancestors 'none' (proti clickjackingu), nosniff, Referrer-Policy (odkaz s tokenem neodejde jinému webu) |
SecurityHeaders.cs |
| Logy a trasy | Do trasy jen enduser.id (ID účtu); v logu nikdy e-mail ani heslo (hlídá test). Serilog loguje cestu bez query stringu, OpenTelemetry query string maskuje. |
SecurityHeaders.cs (UseEndUserTracing) |
Přirovnání: rate limiting je vrátný, který ke dveřím pustí jen pár lidí za minutu; zámek účtu je platební karta, která po třech špatných PINech na chvíli přestane fungovat; CSP je provozní řád, podle kterého prohlížeč nespustí nic, co nepřišlo z MiniShopu.
Otevřený web a osobní údaje
Od účtů kroku 9 je web veřejný – Caddy už nechce heslo (basic auth zůstal jen před Aspire dashboardem). Prohlížet může každý, ale:
- Objednávat jde až s potvrzeným e-mailem. Policy
RequireCustomer= role User a claimminishop:email_confirmed. Robot s vymyšlenou adresou nic neobjedná a e-maily o objednávkách nechodí cizím lidem. - Smazání účtu (právo být zapomenut, GDPR): jméno v hlavičce → Smazat účet → znovu heslo.
sequenceDiagram
autonumber
actor U as Uživatel
participant P as Account/Delete (Web)
participant O as Ordering (ordering.db)
participant A as AccountDeletion (users.db)
U->>P: heslo
P->>P: CheckPasswordSignInAsync (počítá se do zámku účtu)
P->>O: AnonymizeCustomerAsync(ID)
alt nevyřízená objednávka (Pending, Confirmed)
O-->>P: DomainException – nic se nezmění
P-->>U: „Účet nejde smazat, dokud…“
else
O->>O: objednávky: jméno → „Smazaný zákazník“, e-mail → prázdný
O->>O: doručovací adresy smazat (e-shop krok 4)
O->>O: košík zákazníka smazat (e-shop krok 3, stejná transakce)
P->>A: DeleteAsync(user)
A->>A: audit: e-mail → „smazaný účet“, smazat účet (role, claimy cascade)
P->>P: odhlásit
P-->>U: „Účet je smazaný“
end
- Pořadí: nejdřív objednávky, pak účet. Když smazání účtu selže, uživatel to zkusí znovu (anonymizace podruhé nic nepokazí). Obráceně by mohl zůstat smazaný účet s objednávkami, na kterých je pořád jeho e-mail.
- Objednávky zůstanou (položky, ceny, stav – účetnictví a statistiky),
CustomerIdtaké: po smazání účtu sám nikoho neidentifikuje. E-mail o objednávce se na prázdnou adresu neposílá. - Admin si účet smazat nemůže (pravidlo „vždy zůstane admin“) – jiný admin ho nejdřív vrátí na usera.
- Stránka Ochrana osobních údajů (odkaz v patičce a u registrace): co MiniShop ukládá, proč, jak dlouho (zálohy 7 dní), kdo zpracovává (Oracle Cloud, Gmail), jen nezbytné cookies.
Kód: Delete.cshtml.cs,
AccountDeletion.cs, Order.Anonymize
v Order.cs.
Databáze a migrace
users.db jako první databáze přešla na EF Core migrace (účty krok 6 – jsou v ní účty, nesmí se mazat;
ostatní databáze ji následovaly v e-shop kroku 1) (Accounts/Migrations).
Při startu Webu (i příkazu create-admin) AccountsDatabaseInitializer
spustí migrace přes sdílený DatabaseMigrator – ten doplní jen chybějící migrace
a zapamatuje si je v tabulce __EFMigrationsHistory.
- Databáze z kroků 1–5 (vytvořená přes
EnsureCreated) historii nemá – migrátor ji pozná podle tabulkyAspNetUsers, zapíše do ní první migraciInitialAccountsjako provedenou (schéma odpovídá) a pak se doplní jen novější. - Nová migrace (z kořene repozitáře, nástroj
dotnet-efzdotnet-tools.json– poprvédotnet tool restore):dotnet ef migrations add <Nazev> --project src/MiniShop.Infrastructure --context AccountsDbContext --output-dir Accounts/Migrations
Komponentní diagram
flowchart LR
browser(["Prohlížeč<br/>cookie .MiniShop.Auth"])
subgraph web["Web"]
pages["Account/Register, Login, Logout,<br/>ChangePassword, AccessDenied"]
cookie["Cookie middleware<br/>UseAuthentication"]
authz["UseAuthorization<br/>policies RequireAdmin, RequireCustomer"]
signin["SignInManager<br/>vydá a smaže cookie"]
users["UserManager<br/>pravidla hesla, hash, role"]
factory["AccountClaimsPrincipalFactory<br/>ID, e-mail, jméno, role do cookie"]
seeder["AccountsSeeder<br/>role, první admin"]
adminPages["Admin/Users, Detail"]
administration["AccountAdministration<br/>role, blokace, audit"]
stamp["SecurityStampValidator<br/>cookie ↔ databáze každou minutu"]
db[("users.db<br/>AspNetUsers, AspNetRoles,<br/>AspNetUserRoles, AspNetUserClaims,<br/>AccountAuditEntries")]
end
browser -->|"POST formulář"| pages
pages --> signin
pages --> users
signin --> factory
users --> db
seeder --> db
adminPages --> administration
administration --> users
administration --> db
cookie --> stamp
stamp --> db
browser -->|"každý požadavek s cookie"| cookie
cookie --> authz
Registrace a přihlášení
sequenceDiagram
autonumber
actor U as Uživatel
participant P as Account/Register, Login
participant UM as UserManager
participant SM as SignInManager
participant DB as users.db
U->>P: jméno, e-mail, heslo
P->>UM: CreateAsync(user, heslo) + role User
UM->>UM: pravidla hesla (≥ 10 znaků), unikátní e-mail
UM->>DB: uloží uživatele + hash hesla se solí
P->>SM: SignInAsync → cookie s identitou a rolí
Note over U,P: Další požadavky: cookie middleware cookie dešifruje,<br/>do databáze se nedívá (User = přihlášený uživatel)
U->>P: přihlášení e-mailem a heslem
P->>SM: PasswordSignInAsync
SM->>DB: najde uživatele, porovná hash
SM-->>U: cookie (nebo „Neplatný e-mail nebo heslo.“)
Komponenty
| Komponenta | K čemu slouží | Kód |
|---|---|---|
| ApplicationUser | Účet: e-mail (= přihlašovací jméno), hash hesla, zobrazované jméno. Business pravidla nemá, proto nežije v doméně. | ApplicationUser.cs |
| AccountRoles | Názvy rolí User a Admin. |
AccountRoles.cs |
| AccountsDbContext | Tabulky Identity v users.db. |
AccountsDbContext.cs |
| AccountsInfrastructure | Jádro Identity a pravidla: heslo ≥ 10 znaků bez povinných speciálních znaků (NIST), unikátní e-mail. | AccountsInfrastructure.cs |
| AccountsSeeder | Při startu: role, role User pro účty bez role, první admin z konfigurace; totéž pro příkaz create-admin. |
AccountsSeeder.cs |
| AccountsDatabaseInitializer | Migrace users.db (i staré databáze bez historie migrací) a seed. |
AccountsDatabaseInitializer.cs |
| AccountAdministration | Správa uživatelů adminem: role, blokace, pravidla (poslední admin, sám sebe), security stamp, audit. | AccountAdministration.cs |
| AccountAuditEntry | Auditní záznam: kdo, komu, co, kdy (tabulka AccountAuditEntries). |
AccountAuditEntry.cs |
| AccountDeletion | Smazání vlastního účtu (účty krok 9): admin ne, e-mail z auditu pryč, účet i role a claimy smazané. | AccountDeletion.cs |
| CzechIdentityErrorDescriber | České chybové hlášky Identity. | CzechIdentityErrorDescriber.cs |
| AccountClaimsPrincipalFactory | Co se uloží do cookie – navíc zobrazované jméno (role a claimy uživatele přidá Identity). | AccountClaimsPrincipalFactory.cs |
| AddAccounts | Ve Webu: SignInManager, cookie, policies, fallback policy; /api/* dostane 401/403 místo přesměrování. |
AccountsServiceCollectionExtensions.cs |
| Stránky účtu | Registrace, přihlášení, odhlášení (jen POST), změna hesla, přístup odepřen. | Register.cshtml.cs, Login.cshtml.cs, ChangePassword.cshtml.cs |
Zajímavá rozhodnutí
- Žádná vlastní kryptografie. Hashování hesel (PBKDF2 se solí), security stamp, role i budoucí zámek účtu a přihlášení přes Google dodává ASP.NET Core Identity.
- Cookie je
HttpOnly(JavaScript ji nepřečte),Secure(jen HTTPS) aSameSite=Lax(cizí web s ní nepošle formulář). Šifruje se klíči Data Protection – v cloudu leží na volume, takže přihlášení přežije nasazení. - Role jsou v cookie. Proto každá změna role nebo blokace vymění security stamp a Web cookie každou minutu porovná s databází – dotyčný je do minuty odhlášený a novou roli dostane při dalším přihlášení.
- Příznak „musí změnit heslo“ je claim uživatele (tabulka
AspNetUserClaims) – žádný nový sloupec,users.dbse nemaže. - Stejná hláška pro neznámý e-mail i špatné heslo, návrat po přihlášení jen na adresu v MiniShopu
(
Url.IsLocalUrl– ochrana proti „open redirect“), do logu jen ID uživatele. - Ostatní moduly o Accounts nevědí. Ordering bude znát jen ID uživatele (hlídá test hranic modulů).
- Accounts zatím neposílá zprávy, proto
AccountsDbContext(jako Journeys) nedědí ze společnéhoModuleDbContext. - Migrace jen u
users.db. Ostatní databáze jde při změně schématu smazat (data jsou cvičná nebo se obnoví), účty ne.
Dál
- Plán dalších kroků: docs/Ucty-roadmap.md
- Moduly