Вхід
До адмінки веде один шлях, і він пускає один рід учасника — адміністратора платформи. Усім іншим відмовляють на вході, а не на першому екрані за ним.
Чому відмова на вході
«Ви увійшли, але доступу у вас немає» — корисна відповідь тому, хто перебирає паролі. Вона повідомляє, що обліковий запис існує, і що пароль був правильний, і повідомляє це тому, кого все одно не збиралися пускати.
Тому роль перевіряється всередині входу, між перевіркою пароля та видачею токенів. Три різні невдачі повертаються однією відповіддю:
| Що сталося насправді | Що отримує той, хто викликав |
|---|---|
| Такої пошти немає ні в кого | 401 INVALID_CREDENTIALS · Invalid email or password |
| Неправильний пароль | 401 INVALID_CREDENTIALS · Invalid email or password |
| Пароль правильний, але це не адміністратор платформи | 401 INVALID_CREDENTIALS · Invalid email or password |
Той самий статус, той самий код, та сама фраза — і жодного токена в тілі. Адміністратор команди стоїть у третьому рядку: він керує одним орендарем, а керувати одним орендарем не означає керувати платформою.
Три ендпоінти
Усі під admin/auth, тому PlatformAdminGuard накриває їх за оголошенням.
| Маршрут | Захист | Для чого |
|---|---|---|
POST /admin/auth/login | @Public() | Вхід. Відмовляє не-адміністратору сам, до видачі будь-чого. |
POST /admin/auth/refresh | @Public() | Продовження. Перечитує роль із бази, тому її відкликання припиняє продовження одразу. |
GET /admin/auth/me | ґвардія | Питання адмінки на старті. 401 без токена, 403 для дійсного токена, за яким не адміністратор. |
Обидві двері публічні тому, що в того, хто до них прийшов, ще немає токена, і вимагати токен означало б зробити вхід неможливим. Це єдиний отвір, який лишає ґвардія, і саме тому перевірка мусить жити всередині обробника.
Це не друга система облікових записів
Адміністратор — звичайний User із platformRole = 'admin'. Адмінка кличе той самий AuthService, що й кабінет: те саме порівняння bcrypt, той самий секрет, ті самі строки життя — і додає до результату одне питання. Тіла запиту й відповіді — DTO кабінету, без змін; тому ж UserDto і далі не несе платформенну роль: другої користувацької DTO, крізь яку вона могла б витекти, просто немає.
Чому продовження перепитує
Ролі немає в токені. Якби продовження перевіряло лише токен, відкликання ролі набувало б чинності зі спливанням refresh-токена — за місяць. POST /admin/auth/refresh перечитує користувача й відмовляє колишньому адміністраторові з 401 INVALID_TOKEN — тією самою відповіддю, що й мертвому токену, аби відповідь не відрізняла «сесія скінчилася» від «вас розжалували».
З боку адмінки
Кука — не дозвіл
Адмінка й кабінет клієнта живуть на одному хості й говорять з одним api, який приймає токени обох. Клієнт, який увійшов до кабінету й набрав адресу адмінки, приходить із дійсним токеном. Перевірка куки пропустила б його й намалювала оболонку.
Тому адмінка один раз за завантаження документа питає GET /admin/auth/me — до того, як щось намальовано, — і діє за відповіддю. Це ввічливість щодо чесного браузера, а не захист: код працює в браузері відвідувача, і його можна обійти. Чужі дані від клієнта тримає ґвардія в api.
401 і 403 — різні виходи
Вони різняться одним, і це суттєво: чи допоміг би свіжий токен.
- 401 — токена немає або він мертвий. Адмінка продовжує один раз, повторює запит один раз і завершує сеанс лише якщо й повтор відхилено. Це виміряна петля кабінету (AGNT2-72): глухий відмовний шлях коштує двох звернень до api, а не 600+ пар «запит/продовження», як колись.
- 403 — токен у порядку, а людина не адміністратор. Новий токен провалиться так само, тому продовження не відбувається зовсім. Сеанс завершується одразу.
Будь-який із виходів — повна навігація документа на /login, яка скидає всі стори, таймери та запити в польоті, що належали мертвому сеансові, і несе причину в query-рядку:
/login?reason=expired | Your session has expired. Please sign in again. |
/login?reason=forbidden | This account is not an administrator of the platform. |
Причина їде в URL, а не в сторі, бо навігація існує саме для того, щоб стори знищити, — і, на відміну від sessionStorage, URL переживає перезавантаження, посилається й перевіряється тестом. Усе невпізнане в цьому параметрі не малюється взагалі: форма входу не має показувати фразу, яку обрав сторонній.
Куки
agentfy_admin_access і agentfy_admin_refresh — навмисно не кабінетні agentfy_access / agentfy_refresh. На спільному хості одне ім'я означало б, що вихід із кабінету викидає й з адмінки, і — що гірше — що вхід до кабінету тихо наповнює сеанс адмінки токеном, якого вона не видавала.
Реєстрації немає
Адміністратором платформи ніхто не записується сам: роль ставиться записом у базі, поза застосунком (див. сторінку про роль). Маршруту /register в адмінці немає, і посилання на нього на екрані входу теж немає.
Як перевірити руками
За піднятих api та адмінки:
# видати собі роль (ендпоінта для цього немає — так задумано)
cd api && node scripts/platformAdmin.mjs grant you@example.com| Спробуйте | Очікується |
|---|---|
| Відкрити будь-яку адресу адмінки, не входячи | Ви опиняєтесь на /login |
| Увійти звичайним користувачем із правильним паролем | Лишаєтесь на /login, тост INVALID_CREDENTIALS · Invalid email or password |
| Увійти ним же з неправильним паролем | Точно та сама відповідь |
| Увійти адміністратором платформи | Ви всередині, і сторінка називає ваш обліковий запис |
Відкликати роль (platformAdmin.mjs revoke) і перезавантажити | /login?reason=forbidden і фраза на екрані |