Skip to content

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 OTPPOST /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 refreshPOST /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, переключение между двумя организациями, восстановление заархивированной организации, передача владения, принятие приглашения существующим пользователем (автоматизирован только сценарий с совсем новым пользователем), и редактирование набора прав у существующей роли (автоматизировано только назначение роли участнику).