Автентифікація й доступ
Огляд для розробника. Модель прав (діловодства → ролі → дозволи) описана в Архітектурі, тут — маршрути й механіка.
Вхід і сесії
Section titled “Вхід і сесії”AuthController (AuthModule), JWT Bearer з refresh-сесіями.
POST /register/kurin— адмін створює курінь разом з першим акаунтом Звʼязкового;POST /register— акаунт для людини, яка вже є в курені.POST /login— вхід. Якщо в акаунта є другий фактор і пристрій ще не довірений, відповідь несеrequiresMfaі короткоживучийmfaTokenзамість токенів.POST /mfa/login-verify— другий крок: код з автентифікатора або резервний код разом зmfaToken. Успіх ставить cookiemfaTrust(Security:MfaTrustDays, 7 днів), і наступні входи з цього пристрою минають другий крок, поки не зміниться security stamp акаунта.POST /refresh·POST /logout— оновлення й завершення сесії. Сесія — рядок уUserRefreshTokens; вхід додає рядок, вихід завершує один, зміна пароля чи MFA завершує всі. Вихід не чіпаєmfaTrust. Рефреш наново читає діловодства з бази й поруч із токенами віддаєaccess—{ isAdmin, roles, permissions, kurinKey }(IAccessSnapshotFactory, те саме, що й при вході), тож нові права доходять без перезаходу.POST /kurin-scope— вибір активного куреня; токен доступу несе ролі саме для нього.GET /kurin-scope/options— з яких куренів можна обирати (усі активні членства).POST /check-access— серверна перевірка доступу до конкретного ресурсу, якою користується фронтенд, щоб ховати кнопки за тим самим рішенням, що й ендпоінт.
Онбординг: список очікування → запрошення → активація
Section titled “Онбординг: список очікування → запрошення → активація”OnboardingController. Потік: заявка → модерація → запрошення → активація.
POST /waitlist— заявка (зараз лише від Звʼязкового куреня). Кожен адміністратор отримує лист і сповіщення в застосунку; у сайдбарі та на кнопці «Заявки» зʼявляється крапка, поки є що розглянути.GET /waitlist— список для адміна;GET /krais— коди й назви країв для форми, без входу (лише це: кількість куренів бачить адмін уGET api/krai). Заявка несеkraiCode; без нього обовʼязковий вільний текстregionOrCountry, як надсилали старі форми. Невідомий край — 400KraiNotFound;POST /waitlist/{key}/approve— схвалити: створює курінь і акаунт, надсилає запрошення. Необовʼязкове тіло{ "kraiCode": "pl" }замінює край із заявки; без жодного курінь відкривається вKrai:Default. Невідомий край — 400KraiNotFound. Зайнятість числа перевіряється в тому краї, де курінь відкриється: зайняте — 409KurinNumberTaken;POST /waitlist/{key}/reject— відхилити;POST /waitlist/{key}/resend-invitation— надіслати ще раз;POST /invitation/resend— те саме для будь-якого неактивованого акаунта;GET /invitation/{token}/validate— перевірити токен;POST /activate— задати пароль. Відповідь —LoginUserResponseз refresh-cookie: людина одразу в застосунку;POST /password-reset/request·POST /password-reset/reset— відновлення пароля. Запит також надсилає нове запрошення, якщо акаунт ще не активований.
Листи йдуть через IEmailService — див. Пошта. Сутності: WaitlistEntry,
Invitation.
TOTP. GET /mfa/setup (секрет і QR) → POST /mfa/enable (код) → POST /mfa/recovery-codes
(резервні коди, за поточним паролем). GET /mfa/status каже, чи увімкнено і чи вимагається.
Примус для проводу: PrivilegedMfaEnforcementMiddleware + MfaEnforcementPolicy вимагають
другий фактор для адміністратора і Звʼязкового на Production і Staging; Security:EnforcePrivilegedMFA
керує цим на self-host. Ключ E2E:BypassPrivilegedMfa читається лише на тестових тирах. Гейт не ходить
у базу: access-токен несе клейм amr (mfa / pwd, AccessTokenClaims), записаний при видачі —
login, refresh, активація. Увімкнення MFA (mfa/enable) обриває всі сесії й одразу віддає нову
(Tokens у відповіді + cookie), тож старий токен «лише пароль» не доживає до відмови; вимкнення теж
обриває всі сесії, і токен без другого фактора живе щонайбільше Jwt:ExpiresInMinutes. Фронтенд:
mfa-setup-dialog (обовʼязковий діалог після входу), account-settings.
Дозволи на бекенді
Section titled “Дозволи на бекенді”[Authorize(Policy = …)]— грубі політики (RequireUser,RequireAdmin,RequireKurinManagement…).[ResourceAuthorize(ResourceType, ResourceAction, "route:key")]— дозвіл на конкретний ресурс:RolePermissionMapперекладає ролі з діловодств у дозволи зі скоупом (KurinWide,OwnGroups,Own), аAccessContextResolverвизначає, в якому курені людина стоїть і які діловодства тримає.- Активне закріплення впорядника за гуртком (
MentorAssignment) трактується як діловодство впорядника в КВ. - Обидві перевірки покриті тестом
AuthorizationBaselineMatrixTests: кожна дія контролера має бути в матриці, інакше тест падає.
Політика куреня
Section titled “Політика куреня”Частину прав курінь налаштовує сам («Права й видимість»): хто бачить особисті дані, хто дає
перестороги кожного ступеня й підтверджує відзначення кожного рівня, хто видаляє підтверджені
відзначення, підписує й знімає підписи проб, підтверджує вмілості, кому призначає події й задачі
і хто читає журнал дій. Ці можливості
описані в одній таблиці — KurinCapabilities; курінь зберігає лише відхилення від її значень за
замовчуванням у Kurin.AccessPolicy.
- Дозволи людини = статична частина
RolePermissionMap+ гранти політики її куреня (IPermissionResolver, у межах запиту —ICurrentPermissions). Політика кешується на хвилину й скидається при збереженні, тож сервер застосовує її з наступного ж запиту. - Рівні перевіряє обробник (
KurinCapabilities.GiveWarning(level),ConfirmAward(level)), решту —IResourceAccessService.CheckCapabilityAsync. GET api/member/{key}/abilities— що викликач може зробити з цією людиною (ступені пересторог, рівні відзначень, видалення, підпис і зняття підпису, вмілості, журнал). Клієнти беруть кнопки звідси, а не з назв ролей.- Звʼязковий і адміністратор закріплені на повний доступ; сервер відхиляє політику, що це міняє
(400
AccessPolicyInvalid). Так само відхиляється значення, якого рядок не пропонує (CapabilityDescriptor.Reaches):AssignKurinбуває лише «—» або «Увесь курінь». - Події й задачі. Кому призначати — чотири рядки на одну пару
AgendaItem:Create:AssignKurin,AssignGroups,AssignMembers,AssignProvid(провід гуртка — за його гуртком, провід куреня й КВ — лише за «Увесь курінь»).IAgendaAccess.AuthorizeTargetAsyncперевіряє кожну ціль черезCheckCapabilityAsyncу створенні й при додаванні цілі в редагуванні; деревоGET api/agenda/{kurinKey}/assign-targetsбудує ті самі рішення зGetAssignReachAsyncі позначає кожну ціль (canTargetі в людей). Грубої політики за ролями тут нема (RequireUser): хто не має жодного з чотирьох — 403AGENDA_AUTHOR_FORBIDDEN, а клієнтськийAgendaItem:Create:*приходить із політики, тож кнопки «Нова подія/задача» зникають самі.
Коли права змінились
Section titled “Коли права змінились”Токен доступу несе ролі на момент видачі (iat). Коли людині призначають чи знімають офіс або
впорядництво, змінюють членство, роль адміністратора чи політику куреня, сервер позначає це в
IAccessChangeSignal, і AccessStaleHeaderMiddleware додає до відповіді на старий токен
X-Access-Stale: 1. Веб і телефон на цей заголовок тихо роблять рефреш і застосовують access —
сайдбар і кнопки оновлюються без виходу. Позначки живуть у пам’яті процесу; на кількох інстансах
запасні шляхи — рефреш після 403 ResourceAccessDenied (не частіше ніж раз на 30 с) і закінчення
токена (Jwt:ExpiresInMinutes).
| Маршрут | Хто |
|---|---|
GET / PUT api/kurin/{key}/access-policy — права й видимість куреня |
Kurin:Update на цей курінь |
GET api/member/{key}/abilities — що викликач може зробити з людиною |
Member:Read на цю людину |
GET api/kurin/{key}/journal?area&memberKey&actorUserKey&fromUtc&toUtc&skip&take — журнал дій |
KurinJournal:Read; охоплення (весь курінь чи свої гуртки) — з політики |
Краї й картка куреня
Section titled “Краї й картка куреня”| Маршрут | Хто |
|---|---|
api/krai/* — краї (додати, перейменувати), профіль краю, його ревізії й відновлення давнішої (KraiController) |
RequireAdmin на весь контролер |
GET api/krai/{code}/dues/concessions?year=&number= — скільки кожна пільга краю зняла з вкладки його куренів за квартал (без кварталу — поточний за часом краю) |
RequireAdmin (той самий контролер) |
PUT api/kurin/{key}/krai — перевести курінь до іншого краю (число має бути вільне там) |
RequireAdmin |
POST api/group/{groupKey}/dues/members/{membershipKey}/concessions — надати людині пільгу краю; PUT / DELETE api/group/{groupKey}/dues/concessions/{grantKey} — змінити чи прибрати надану |
GroupDues:Update на цей гурток |
GET / PUT api/kurin/{key}/card-fields — які поля картки курінь приховує |
Kurin:Update на цей курінь |
GET api/kurin/{key}/profile — профіль краю, за яким живе курінь, з урахуванням його прихованих полів |
Kurin:Read на цей курінь |
GET api/kurin/{key}/catalog/badges — вмілості, які пропонує курінь |
Kurin:Read на цей курінь |
Особисті дані людини
Section titled “Особисті дані людини”Пошта й телефон, дата народження (з роком і окремо день із місяцем), адреса й школа, контакти
батьків і поля краю (CardExtras) — шість груп з MemberDataFields. Хто бачить кожну, вирішує
політика куреня (See*): нікому, свій гурток (гуртки, які людина веде, і її власний) чи увесь курінь.
- Маска одна —
MemberFieldMaskзIMemberFieldAccess.ForViewerAsync. Її накладають картка (GetMemberByKeyQuery), списки (GetMembersQuery, звідти й вивантаження реєстру), досьє, верифікація профілю й відповідь на збереження. Маскування в SQL немає. - Приховане приходить порожнім, а відповідь несе
hiddenData(назви груп) іbirthday(MM-dd) — клієнт не рендерить полів прихованих груп ні в картці, ні у формі (порожнє неприховане лишається «Не вказано»). - Своє людина бачить завжди. Публічний код (
PublicId) — лише вона сама: провід, що знав би код, міг би записати її в інший курінь, не спитавши. Статус акаунта (AccountStatus) — тим, хто може редагувати цю людину (Member:Update). - Запис: група, прихована від того, хто редагує, вважається ненадісланою — збережене значення
лишається, обовʼязковість не перевіряється (
HiddenDataOnWrite). - PDF-звіт куреня не маскується: його бачать лише ті, хто керує всім куренем, а вони закріплені на повний доступ (тест це тримає).
Guard-и (Angular)
Section titled “Guard-и (Angular)”authGuard— вимагає автентифікації;kurinAccessGuard('kurin' | 'panel')— активний курінь у скоупі, або адмін-панель;capabilityGuard('admin' | 'kurinManagement' | 'groupLeadership')— можливість за роллю, ті самі поняття, що й політики на бекенді;leadershipAccessGuard— сторінка складу проводу: курінний бачить провід куреня, гуртковий — свого гуртка, КВ не редагується тут узагалі (PermissionService.canSetupLeadership);EntityGuard— доступ до конкретної сутності черезcheck-access, fail-closed: помилка сервера означає «заборонено»;setupGuard— майстер первинного налаштування;publicAuthRedirectGuard— вже автентифікованих з публічних сторінок веде додому.
Принцип: guard-и ховають, бекенд вирішує. Жодна перевірка на фронті не є останньою.
Точкування й справи
Section titled “Точкування й справи”Маршрути точкування (api/kurin/{key}/score/*) пускає будь-кого з куреня, а хто що може
всередині, вирішує обробник через ScoreAccess — для кожної людини й гуртка окремо.
| Маршрут | Хто |
|---|---|
PUT / DELETE api/kurin/{key}/score/events/{itemKey}/{occurrence}/not-held — «Сходин не було» і зняти це |
той, хто може відмітити присутність бодай одного юнака куреня (GroupScore:Create на гурток або на неприписаних); PUT відмовляє 409 OccurrenceHasAttendance, поки хтось відмічений |
GET api/me/duties — справи проводу (items, hiddenCount) |
RequireUser; кожна справа — лише там, де офіси людини дають на неї право |
POST api/me/duties/dismissed { key, fingerprint } — сховати справу для себе; DELETE api/me/duties/dismissed?key= — повернути одну чи (без key) усі |
RequireUser; стосується лише рядків самого викликача |
Журнал дій
Section titled “Журнал дій”KurinAuditEvent — хто, що й коли змінив у курені. Обробник пише запис через
IKurinAudit.RecordAsync перед власним SaveChangesAsync, тож запис і зміна комітяться разом. Дія —
рядок з AuditActions, розділ — AuditArea; зміни — AuditChanges (лише змінене, особисті поля як
«змінено» без значень). Історія на картці проби й вмілості читається з того ж журналу
(IKurinJournalReader). Строк — 365 днів, див. зберігання.
Локальні дев-інструменти
Section titled “Локальні дев-інструменти”На Development, E2E, Tailscale є DevToolsController (api/dev/impersonate,
api/dev/impersonate/member, api/dev/return) і перемикач ролей на правому краю екрана: адмін
заходить за діловодство у курені або як людина з відкритої картки і повертається квитком. Квиток
живе 12 годин; прострочений фронт викидає й позичає роль на поточній сесії, замість того щоб падати
на кожному перемиканні. На інших тирах контролер знімається з моделі
(DevOnlyControllerFeatureProvider).