Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 45s
CI / Web (React + Vite) (push) Successful in 40s
Docker API / Build + push API (push) Successful in 1m7s
Docker Web / Build + push Web (push) Successful in 32s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
— «Владелец» переименован в «Главный администратор» — терминологически
у нас не «собственник», а тот кто управляет организацией.
Бейдж в таблице, тексты модалок, серверные сообщения 403 — везде
единая формулировка.
— PUT /api/organization/employees/{id}: добавлен гард для главного
администратора:
· Смена RoleId на не-«Администратор» → 403 «Нельзя сменить роль…»
· IsActive=false → 403 «Нельзя деактивировать…»
Раньше юзер мог поменять себе роль на Кладовщик и получить бейдж
«Владелец» с ролью кладовщика — несостыковка.
— EmployeesPage: при редактировании главного администратора
· Селект ролей disabled + amber-плашка-объяснение «роль фиксирована»
· Чекбокс «Активен» disabled + текст «нельзя деактивировать»
· save() ловит ошибки и показывает их в общей модалке (раньше 403
«тихо проваливалось» — модалка зависала).
— recovery-restore-orphan-owners.sql добавлен блок: для всех
Organizations где главный администратор имеет роль не-«Администратор»
или IsActive=false → восстанавливает «Администратор» и активирует.
Идемпотентен. Применён на стейдже (0 пострадавших — текущая БД ОК).
Все изменения главного администратора (роль, ФИО, удаление, передача
управления) по архитектурному решению юзера должны идти через очередь
запросов к Супер-администратору платформы. Эта подсистема — отдельная
большая фича (RequestType / RequestQueue / SuperAdmin approval UI),
её план описан в TG-ответе.
73 lines
2.8 KiB
PL/PgSQL
73 lines
2.8 KiB
PL/PgSQL
-- Recovery: orphan AppUser cleanup.
|
||
--
|
||
-- Применяется один раз вручную на стейдже/проде после деплоя
|
||
-- AuthorizationController + SuperAdminOrganizationsController фиксов
|
||
-- (audit 2026-04-27 #1, #2, #7).
|
||
--
|
||
-- Что делает:
|
||
-- 1. Находит users у которых OrganizationId указывает на отсутствующую
|
||
-- или архивированную организацию.
|
||
-- 2. Деактивирует таких users (IsActive=false), сбрасывает OrganizationId.
|
||
-- 3. Отзывает все OpenIddict refresh/access токены этих users
|
||
-- (Status='revoked') чтобы существующие сессии оборвались.
|
||
--
|
||
-- Идемпотентен: повторный запуск ничего не ломает.
|
||
-- Не удаляет данные — только статусы. Юзер при необходимости может
|
||
-- быть восстановлен ручным UPDATE users SET "IsActive"=true.
|
||
|
||
BEGIN;
|
||
|
||
WITH orphan_users AS (
|
||
SELECT u."Id"
|
||
FROM users u
|
||
LEFT JOIN organizations o ON o."Id" = u."OrganizationId"
|
||
WHERE u."IsActive" = true
|
||
AND (
|
||
u."OrganizationId" IS NULL
|
||
OR o."Id" IS NULL
|
||
OR o."IsArchived" = true
|
||
)
|
||
AND NOT EXISTS (
|
||
-- Не трогаем SuperAdmin'ов — у них org=null это норма.
|
||
SELECT 1
|
||
FROM "AspNetUserRoles" ur
|
||
JOIN roles r ON r."Id" = ur."RoleId"
|
||
WHERE ur."UserId" = u."Id" AND r."NormalizedName" = 'SUPERADMIN'
|
||
)
|
||
)
|
||
UPDATE users
|
||
SET "IsActive" = false,
|
||
"OrganizationId" = NULL
|
||
WHERE "Id" IN (SELECT "Id" FROM orphan_users);
|
||
|
||
UPDATE "OpenIddictTokens" t
|
||
SET "Status" = 'revoked'
|
||
WHERE t."Status" = 'valid'
|
||
AND t."Subject" IN (
|
||
SELECT u."Id"::text FROM users u
|
||
WHERE u."IsActive" = false
|
||
);
|
||
|
||
-- Owner-Employee должен оставаться в роли «Администратор» и быть IsActive=true.
|
||
-- Если кто-то сменил роль владельца на Кладовщика/Менеджера/Кассира или
|
||
-- деактивировал — возвращаем «Администратор» и активируем.
|
||
WITH admin_role_per_org AS (
|
||
SELECT r."OrganizationId", r."Id" AS role_id
|
||
FROM employee_roles r
|
||
WHERE r."IsSystem" = true AND r."Name" = 'Администратор'
|
||
)
|
||
UPDATE employees e
|
||
SET "RoleId" = ar.role_id,
|
||
"IsActive" = true,
|
||
"FiredAt" = NULL
|
||
FROM organizations o
|
||
JOIN admin_role_per_org ar ON ar."OrganizationId" = o."Id"
|
||
WHERE e."OrganizationId" = o."Id"
|
||
AND e."UserId" = o."AccountOwnerUserId"
|
||
AND (
|
||
e."RoleId" <> ar.role_id
|
||
OR e."IsActive" = false
|
||
);
|
||
|
||
COMMIT;
|