IAM — авторизация, организации, роли и права
Это модуль, с которого начинается любая сессия: подтвердить, кто ты, а дальше выбрать (или оказаться помещённым в) организацию. Всё ниже опирается на apps/core-api/src/modules/auth и .../organizations, .../roles.
Как сделать...
Сгруппировано так же, как технические разделы ниже — иконка помощи каждой страницы ведёт прямо в свою группу, а не во весь этот список.
Аутентификация
Создать аккаунт — /auth/sign-up → Email, Пароль → Создать аккаунт → проверь почту — там письмо со ссылкой подтверждения. См. гайд по полям ниже.
Войти без пароля — на /auth/sign-in нажми Войти по magic link или Войти по коду вместо пароля (переключатели рядом с формой пароля), впиши email, проверь почту. См. гайд по полям ниже.
Войти через Google — Продолжить с Google на /auth/sign-in.
Восстановить забытый пароль — Забыли пароль? на /auth/sign-in → впиши email → перейди по ссылке из письма. См. гайд по полям ниже.
Создать аккаунт — гайд по полям
- Email (обязательно) — должен выглядеть как настоящий email-адрес. Если он уже зарегистрирован, тебе прямо об этом сообщат при отправке — в отличие от беспарольных сценариев ниже, регистрация не скрывает, есть ли уже аккаунт на этот адрес.
- Пароль (обязательно) — от 8 до 128 символов. Других требований к сложности нет (не нужны обязательные заглавные буквы/цифры/символы).
- После отправки приходит письмо с подтверждением и появляется ссылка Отправить письмо повторно — ограничена 3 отправками в минуту; при превышении показывается отдельное сообщение об ограничении вместо общей ошибки. Регистрация также незаметно создаёт для тебя личное рабочее пространство в фоне (см. Что такое P4P) — на самом экране об этом ничего не сказано.
Войти без пароля — гайд по полям
Оба переключателя — magic link и одноразовый код — полностью заменяют форму пароля (не добавляют второе поле к ней) — на один визит выбирается один способ, а не оба сразу.
- Magic link: Email (обязательно, проверяется только непустота — формат на клиенте не проверяется) → Отправить ссылку. Что бы ты ни ввела, текст подтверждения всегда один и тот же — «Если такой аккаунт существует, ссылка для входа отправлена» — приложение намеренно никогда не подтверждает и не опровергает, зарегистрирован ли адрес, так что опечатка в email покажет ровно то же сообщение об успехе, что и настоящий адрес. Сама ссылка действует 15 минут.
- Одноразовый код: Email (то же нераскрывающее поведение, что у magic link) → Отправить код → появляется поле 6-значный код (только цифры, ровно 6 знаков — кнопка Подтвердить остаётся неактивной, пока не введены все 6). Код действует 10 минут, и действителен только самый последний отправленный код — запрос нового мгновенно делает недействительным предыдущий.
- Оба способа делят один и тот же лимит в 3 отправки в минуту, что и любое другое действие отправки писем на этой платформе, показанный как обратный отсчёт на кнопке повторной отправки.
Восстановить забытый пароль — гайд по полям
- Запрос (
/auth/forgot-password): Email (обязательно, с проверкой формата) → Отправить ссылку для сброса. То же нераскрывающее сообщение-подтверждение, что и у беспарольных способов входа выше — по одному этому экрану невозможно понять, есть ли у введённого адреса аккаунт на самом деле. - Задать новый пароль (ссылка из письма,
/auth/reset-password?token=…): Новый пароль (обязательно, 8–128 символов) и Подтверждение пароля (обязательно) — они должны совпадать в точности, проверка привязана именно к полю Подтверждение пароля (там и появляется ошибка о несовпадении, не у Нового пароля). Отсутствующий токен (переход на страницу вообще без ссылки) заменяет всю форму простым сообщением об ошибке; неверный или уже использованный токен ловится только при отправке и показывается строкой под формой, как любая другая ошибка отправки.
Личный кабинет
Отредактировать профиль — /account/profile → обнови фото, имя, телефон, часовой пояс, био, локацию, дату рождения, навыки и области интересов.
Принятие приглашения
Принять приглашение от коллеги — открой ссылку из письма-приглашения (/invitations/[token]) → если аккаунта ещё нет, автоматически появится поле для пароля; если есть — просто подтверди.
Создание организации
Кнопки для этого сегодня нет — см. Создание организации ниже про единственный косвенный способ, которым это происходит (автоматическое личное пространство при регистрации, или диалог «New workspace» у платформенного администратора).
Настройки организации
Изменить название/описание/сайт организации — «Настройки» → отредактируй поля → Сохранить изменения. См. гайд по полям ниже.
Изменить настройки организации — гайд по полям
Без portal:organization:manage (по умолчанию не делегировано каждой роли — см. Роли ниже) вся эта страница показывает те же три значения просто как обычный текст вместо формы — здесь нет неактивной/серой формы для сигнала «можно смотреть, нельзя трогать», полей просто нет.
- URL пространства (показан над формой, никогда не редактируется) — задаётся один раз при создании и остаётся навсегда; нигде в продукте нет поля или действия, которое бы его меняло впоследствии.
- Название (обязательно) — от 2 до 100 символов.
- Описание (необязательно) — до 500 символов.
- Сайт (необязательно) — если что-то вписать, это должен быть полный, корректный URL (например,
https://example.com) — голый домен без схемы отклоняется. Оставить пустым можно — это единственное поле формы, которое действительно необязательно в обе стороны. - Чего здесь не найти, хотя может показаться логичным: смена владельца организации (это Передать владение, действие в строке на списке Участники ниже, только для OWNER) и архивирование/восстановление организации (для этого сегодня вообще нет интерфейса — см. технический раздел ниже).
Участники
Пригласить кого-то в свою организацию — «Участники» → Пригласить участника → Email, выбери Роль → Отправить приглашение. См. гайд по полям ниже.
Удалить участника или передать владение — список «Участники» → меню действий строки → Передать владение (может только текущий OWNER) или удаление.
Назначить участнику роль или изменить его должность — открой страницу этого участника (не список) → раздел Роли или Должность в этом рабочем пространстве → Сохранить. См. гайд по полям ниже.
Пригласить участника — гайд по полям
- Email (обязательно) — проверки формата на клиенте нет; некорректный адрес ловится только при отправке. Адрес, который уже участник или уже имеет ожидающее приглашение, тоже ловится только тогда.
- Роль (обязательно, одиночный выбор) — в отличие от приглашения в персонал платформы, участник организации никогда не может остаться совсем без роли; список по умолчанию предлагает MEMBER (или ту роль, что оказалась первой, если роли MEMBER вообще нет), так что поле никогда случайно не остаётся пустым. OWNER никогда не появляется среди вариантов — единственный способ сделать кого-то владельцем — Передать владение, но не приглашение.
- Отправить приглашение ограничено 3 отправками в минуту, то же поведение с обратным отсчётом, что и у любого другого диалога приглашения на этой платформе.
Роли и должность участника — гайд по полям
- Должность в этом рабочем пространстве (необязательно, обычный текст, до 100 символов) — единственное поле на этой странице, которое ты всегда можешь редактировать у своего собственного членства, независимо от роли или прав; редактирование должности другого человека требует
portal:members:manage. Это привязано к организации, а не глобально — один и тот же человек может иметь разную должность в каждом рабочем пространстве, где он состоит. - Роли (чекбоксы, требует
portal:members:manage) — любое количество, включая ноль (снятие всех ролей не удаляет само членство, просто оставляет без прав). OWNER тоже никогда не появляется в этом списке — по той же причине, что и в диалоге приглашения: она перемещается только через Передать владение, никогда не отмечается здесь галочкой. - Сохранить на каждой из двух панелей активируется только когда в этой конкретной панели что-то реально изменилось — они независимы: правка должности не требует также трогать Роли, и наоборот.
Роли
Создать свою роль для организации — «Роли» → Создать роль → назови, выбери права. См. гайд по полям ниже.
Отредактировать, дублировать или удалить роль — меню ⋯ роли → Редактировать, Дублировать или Удалить. Четыре встроенные роли (Owner/Admin/Member/Viewer) нельзя переименовать или удалить — их пункт Редактировать даже не появляется в меню, в отличие от аналогичного экрана платформенных ролей, где Редактировать остаётся доступным на системных ролях с заблокированным только названием (см. Администрирование платформы → Роли); Дублировать по-прежнему работает и на них, как на любой роли. Тот же гайд по полям ниже для Создать/Редактировать.
Создать/отредактировать роль организации — гайд по полям
Структурно та же форма, что и у платформенной роли, с двумя реальными отличиями, о которых стоит знать:
- Название (обязательно) — должно быть уникальным только внутри этой организации, не по всей платформе; у двух разных организаций может быть роль с буквально одинаковым названием «Billing Manager» без конфликта. Диалог дополнительно отказывается принимать меньше 2 символов, хотя один только бэкенд принял бы и один символ. До 100 символов.
- Описание (необязательно) — до 500 символов.
- Права (чекбоксы, сгруппированы по областям) — минимума нет. У четырёх встроенных ролей этот раздел просто вообще недостижим (для них нет действия «Редактировать», как отмечено выше) — нет отдельного «просмотра прав только для чтения», в отличие от платформенной роли, где системные строки открывают диалог с заблокированным только Названием.
- Удалить дополнительно отказывается удалять роль, которая всё ещё назначена активному членству или ожидающему приглашению — сначала переназначь всех на другую роль. У платформенных ролей такой проверки нет.
Аутентификация (/auth/*)
Простыми словами: это всё, что находится под экраном «Sign In» / «Sign Up» — создание аккаунта, вход пятью разными способами (пароль, ссылка на email, одноразовый код на email, Google, или сброс забытого пароля), и выход. Техническую часть ниже можно не читать, если ты не дебажишь один из этих сценариев.
Все эти роуты помечены @Public() в auth.controller.ts (сессия не нужна, чтобы до них достучаться), а «тяжёлые на запись» (register, login, magic-link, password-reset/request, otp/send) ещё несут @UseGuards(ThrottlerGuard) — если скриптуешь против них многократно, жди рейт-лимит (см. docs/TESTING.md, прежде чем вообще что-то скриптовать).
- Sign up / Sign in (
/auth/[mode].vue,mode=sign-in|sign-up) — одна страница, два режима, переключаются параметром роута. Sign-up бьёт вPOST /auth/register, который создаётUser+PERSONALорганизацию для него (см. Что такое P4P — у каждого пользователя есть личный рабочий кабинет) и отправляет письмо с подтверждением через Mailpit в dev. Sign-in бьёт вPOST /auth/login, защищёнLocalAuthGuard(email+пароль). - Verify email (
/auth/verify-email) — принимает токен из письма с подтверждением (POST /auth/verify-email).POST /auth/resend-verificationстоит за действием «отправить ещё раз», если ссылка истекла. - Forgot / Reset password (
/auth/forgot-password,/auth/reset-password) —POST /auth/password-reset/request(всегда отвечает успехом, независимо от того, существует ли email — не считай ответ «проверь почту» подтверждением существования аккаунта), затемPOST /auth/password-reset/confirmс токеном из письма. - Magic link (
/auth/magic-link) — вход без пароля.POST /auth/magic-linkотправляет письмо,POST /auth/magic-link/verifyпринимает токен и начинает сессию — та же форма токена, что и при входе по паролю. - Email OTP —
POST /auth/otp/send/POST /auth/otp/verify. Отдельной страницы нет — доступ через переключатель («Войти по коду вместо пароля») прямо на/auth/sign-in(состояниеsignInMethodвLoginForm.vue), рядом с таким же переключателем для Magic Link. - Google OAuth (
/auth/callback,/auth/error) —GET /auth/googleзапускает OAuth-рукопожатие (GoogleAuthGuard),GET /auth/google/callbackзавершает его и перенаправляет на/auth/callback(успех) или/auth/error(неудача — истёкшее/отклонённое согласие, конфликт аккаунтов и т.п.). - Logout (
/auth/logout) —POST /auth/logoutотзывает refresh-токен; access-токены короткоживущие (15 мин), так что серверный отзыв для них не нужен. - Session refresh —
POST /auth/refresh— не страница, вызывается автоматически HTTP-клиентом портала, когда истекает access-токен. Refresh-токены непрозрачные, живут 30 дней, ротируются, с обнаружением кражи (повторное использование уже ротированного токена отзывает всю цепочку) — см.docs/iam/ADR-001-authentication.md.
Личный кабинет (/account/profile)
Страница профиля уровня User — отображаемое имя, аватар (POST/DELETE /me/avatar) и другие поля на GET/PATCH /me. Не привязана ни к одной организации; см. Архитектурные уровни.
Принятие приглашения (/invitations/[token])
Простыми словами: страница, на которую коллега попадает, кликнув по ссылке в письме-приглашении. Работает независимо от того, есть ли у него уже аккаунт P4P — если нет, автоматически появляется форма email/пароль.
Публичная страница (GET /organizations/invitations/:token, POST /organizations/invitations/:token/accept) — работает и для уже вошедшего пользователя, присоединяющегося к другой организации, и для совсем нового человека, который вводит email/пароль, чтобы создать аккаунт и присоединиться за один шаг. Приглашения несут набор Role (InvitationRole), назначаемых при принятии через MembershipRole.
Создание организации
POST /organizations требует только аутентифицированного пользователя — без особого права. Любой вошедший пользователь может создать дополнительные организации сверх своей автоматически созданной личной (например, чтобы представить компанию, которую он основывает). Подтверждено (2026-08-04, поиск по apps/portal по обоим API-путям): в интерфейсе портала для этого сегодня вообще нет UI — ни страницы /org/create, ни диалога «новая организация», доступного с /dashboard. Единственный способ дойти до POST /organizations из UI сейчас — косвенный: как побочный эффект регистрации (которая создаёт автоматическую PERSONAL организацию) или из диалога «New workspace» в Администрировании платформы (доступно только там). Самостоятельный сценарий «создать ещё одну организацию» готов на бэкенде, но пока не подключён к фронтенду.
Настройки организации (/org/[slug]/settings)
Простыми словами: страница Settings внутри организации сегодня позволяет редактировать только название, описание и сайт — и ничего больше. Две вещи, которые ты могла бы ожидать здесь найти — «передать владение кому-то другому» и «заархивировать/восстановить эту организацию» — либо живут в другом месте, либо пока не существуют в интерфейсе (подробности ниже).
- Поля профиля (название, описание, сайт) —
PATCH /organizations/:id, закрыто правомportal:organization:manage, делегируемо ADMIN или кастомной роли. Это всё, что предоставляетsettings.vue— подтверждено чтением файла (2026-08-04), у неё нет элементов управления архивацией/активацией. - Передача владения — несмотря на то, что это действие уровня жизненного цикла организации, его нет на странице Settings. Это действие по строке на странице Участники (
workspace.members.actions.transferOwnership,members/index.vue) — «сделать этого участника владельцем вместо меня».PATCH /organizations/:id/transfer-ownership, закрыто толькоTenantGuardна уровне роута, с реальной проверкой (assertActorIsOwner()) внутриorganizations.service.ts— не делегируется черезportal:organization:manage; нужно реально занимать рольOWNER. - Архивация / активация (
POST /organizations/:id/archive,PATCH /organizations/:id/activate) — подтверждено (2026-08-04, поиск поapps/portalпо обоим путям): точки входа в интерфейсе нет нигде. Оба роута работают (то же ограничение толькоTenantGuard+assertActorIsOwner(), что и у передачи владения) и доступны прямым API-вызовом, но сейчас в портале нет кнопки/диалога, который бы их вызывал — OWNER не может самостоятельно заархивировать свою организацию через UI сегодня. Единственный UI портала, близкий к архивации — это действие restore в Администрирование платформы → Организации (обратное направление, доступно только там). - Slug неизменяем и не имеет пути редактирования (та же логика, что и у
User.email).
Участники (/org/[slug]/members, /org/[slug]/members/[userId])
Простыми словами: список участников — это место, где приглашают людей, удаляют их или передают владение. Чтобы поменять роль или job title конкретного человека, нужно зайти на его собственную страницу — эти два действия не на самом списке.
- Список (
GET /organizations/:id/members), с выпадающим меню «Действия» по строке (portal:members:readдля самого списка; видно любой роли, включая VIEWER). - Приглашение (
POST /organizations/:id/members/invite) —portal:invitations:manage. Throttled (ThrottlerGuard) в дополнение кTenantGuard/PermissionsGuard. - Удаление участника и передача владения — оба живут на странице списка (
members/index.vue), как действия по строке в выпадающем меню, со своими диалогами подтверждения. Удаление требуетportal:members:manage; передача владения — только для OWNER (см. Настройки организации выше — это действие жизненного цикла, хоть и живёт на этой странице, а не на Settings). - Назначение/снятие роли, изменение job title — оба живут на странице карточки участника (
members/[userId].vue), не на списке.POST/DELETE .../members/:userId/roles/:roleIdиPATCH .../members/:userId/job-titleтребуютportal:members:manage; изменение своего собственного job title (PATCH .../members/me/job-title) требует толькоTenantGuard— повышенное право не нужно, чтобы поменять свой отображаемый титул. - Выход из организации (
DELETE /organizations/:id/leave) — подтверждено (2026-08-04, поиск поapps/portal): точки входа в интерфейсе нет. Роут на бэкенде работает (толькоTenantGuard, любой может убрать сам себя, заблокировано для единственного/последнего OWNER согласноdocs/iam/ADR-007-security-invariants.md), но кнопки «Выйти из организации» сегодня нет нигде в портале. Не путать сSetOnLeaveDialog.vue— это несвязанная фича Staffing (отметить человека в отпуске), а не выход из организации.
Роли (/org/[slug]/roles)
Простыми словами: роли — это именованные наборы прав (например, «может приглашать людей» или «может редактировать название организации»), которые назначаются участникам. Четыре роли встроены и не могут быть переименованы или удалены — Owner, Admin, Member, Viewer — и поверх них можно создавать свои.
roles.controller.ts, смонтирован на organizations/:id/roles. Каждый роут требует @UseGuards(TenantGuard, PermissionsGuard) именно в этом порядке — см. заметку в CLAUDE.md/docs/iam/PERMISSIONS_CATALOG.md о том, почему PermissionsGuard никогда не может быть зарегистрирован глобально.
- Список ролей + список назначаемого каталога прав (
GET /organizations/:id/roles,GET .../roles/permissions) —portal:roles:read. - Создание / редактирование / удаление кастомной роли, добавление/удаление права у роли (
POST,PATCH :roleId,DELETE :roleId,POST/DELETE :roleId/permissions/:permissionId) — всё требуетportal:roles:manage. Системные роли (OWNER/ADMIN/MEMBER/VIEWER) защищены от удаления/переименования триггером БД, а не только проверкой в приложении — см.docs/iam/ADR-003-rbac.md.
MEMBER и VIEWER намеренно идентичны в посеянном наборе прав сегодня (оба только для чтения) — это задокументированное, повторно подтверждённое решение, а не пробел. Не «исправляй» это, не прочитав обоснование в docs/iam/PERMISSIONS_CATALOG.md.
Impersonation
Простыми словами: позволяет SUPER_ADMIN временно увидеть платформу глазами другого пользователя — чтобы помочь отладить его аккаунт или воспроизвести проблему, о которой он сообщает. Каждое использование логируется. Кнопки для этого сейчас нет нигде — см. подтверждённую находку ниже.
Только для SUPER_ADMIN (SuperAdminGuard — буквальная проверка роли, а не делегируемое право, поскольку impersonation намеренно не делегируется). POST /auth/impersonate начинает сессию от имени другого активного пользователя; POST /auth/impersonate/:sessionId/end её завершает. Каждый старт/финиш/отказ записывается в журнал аудита (impersonation.started/.ended/.rejected).
Подтверждено (2026-08-04, поиск по apps/portal по слову impersonat и отдельно по /auth/impersonate): фронтенда для этого нет вообще. Не кнопка, скрытая за правом — строка impersonat встречается в исходниках портала ровно дважды, оба раза случайно (опция фильтра по домену AuditLog на /platform/audit и комментарий в коде), и ничто не вызывает POST /auth/impersonate. Это полностью серверная функция сегодня — доступна только прямым API-вызовом с сессией SUPER_ADMIN, не через какую-либо страницу в Администрировании платформы или где-либо ещё. Если нужно кого-то impersonate прямо сейчас — UI-пути нет, только сырой аутентифицированный запрос.
Журнал аудита
Простыми словами: постоянная, защищённая от подделки история важных для безопасности действий по всей платформе — кто вошёл, кто поменял роль, кто заархивировал организацию. Никто, включая SUPER_ADMIN, не может отредактировать или удалить запись после того, как она записана.
Каждое событие в этом модуле (вход, смена роли, жизненный цикл организации, impersonation...) записывается в AuditLog, доступный только на добавление, через AuditService (никогда напрямую через Prisma — триггер БД блокирует update/delete). UI просмотра (/platform/audit, platform:audit:read) — это страница Администрирования платформы — см. Администрирование платформы → Журнал аудита.
Тестирование этого модуля
scripts/e2e/menu/auth.mjs, organizations.mjs и impersonation.mjs (38 проверок всего) прогоняют сценарии этого модуля от начала до конца и проставляют lastVerified выше. Не покрыто автоматизацией, стоит проверить руками — см. docs/MANUAL_TESTING.md: Google OAuth, переключение между двумя организациями, восстановление заархивированной организации, передача владения, принятие приглашения существующим пользователем (автоматизирован только сценарий с совсем новым пользователем), и редактирование набора прав у существующей роли (автоматизировано только назначение роли участнику).