food-market/deploy/recovery-restore-orphan-owners.sql
nns 633bdf3ef0
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 46s
CI / Web (React + Vite) (push) Successful in 41s
Docker API / Build + push API (push) Successful in 1m12s
Docker Web / Build + push Web (push) Successful in 31s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
fix(auth): закрыть критические дыры — orphan login, self-delete, owner-delete, override-баннер
Аудит 2026-04-27. Полный отчёт — docs/audit-2026-04-27.md.

Что закрыто:

— /connect/token (AuthorizationController) теперь отказывает в login если
  AppUser привязан к удалённой/архивной Organization. SuperAdmin обходит
  проверку (ему org не нужна). Жалоба: nurnetps@gmail.com мог логиниться
  после удаления своей org из SuperAdmin консоли.

— SuperAdminOrganizationsController.Delete (DELETE org) каскадно
  деактивирует всех AppUser привязанных к этой org (IsActive=false,
  OrganizationId=null) и помечает Status='revoked' для всех их
  OpenIddictTokens. Раньше Org удалялась, а юзеры оставались валидными
  с активными refresh-tokens на 30 дней.

— EmployeesController.Delete теперь soft-delete (IsActive=false,
  FiredAt). Запрещены: 403 если попытка удалить себя; 403 если
  попытка удалить Owner (Organization.AccountOwnerUserId ==
  employee.UserId). Сообщения с инструкцией («передайте права»,
  «покинуть через настройки»).

— /api/me возвращает hasLiveOrg и hasActiveEmployee — frontend
  использует это для редиректа на /no-organization вместо белого экрана.

— Новая страница /no-organization (NoOrganizationPage) — fallback для
  orphan AppUser. CTA: создать новую org через публичный /signup
  или попросить инвайт. Кнопка «выйти». TenantRouteGuard редиректит
  orphan юзеров туда.

— SuperAdminAsOrgBanner: добавлена проверка через useMe — баннер
  рендерится только если у текущего юзера есть Identity-роль
  SuperAdmin. Lingering localStorage override от прошлой сессии
  (другой юзер логинился до этого) автоматически чистится.

— auth.ts: clearTokens() теперь сбрасывает superAdminAsOrg и
  superAdminEditMode. login() вызывает clearTokens() ПЕРЕД запросом
  чтобы новый юзер не унаследовал override-состояние от предыдущего.

— deploy/recovery-restore-orphan-owners.sql — идемпотентный скрипт
  деактивирующий уже накопленных orphan AppUser (как nurnetps) и
  revoke их токены. Применён на стейдже: 1 user деактивирован,
  9 токенов revoked.

— deploy/Dockerfile.api: убран `--no-restore` из publish — два
  раздельных шага роняли build с NETSDK1064 на свежих analyzer-
  зависимостях, теперь restore идёт внутри publish.

Smoke (стейдж):
- nurnetps@gmail.com /connect/token → invalid_grant.
- admin@food-market.local /connect/token → access_token выдан.
- food-market.zat.kz/, /signup/, app.../login, /health → 200.
2026-04-27 09:28:18 +05:00

52 lines
1.9 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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
);
COMMIT;