From 5091d43f5d8f2a8a9aedaf682dd3a2055b0c665c Mon Sep 17 00:00:00 2001 From: nns Date: Tue, 26 May 2026 11:47:56 +0500 Subject: [PATCH] =?UTF-8?q?fix(employees):=20=D1=83=D0=B2=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5/=D0=B4=D0=B5=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B3=D0=B0=D1=81?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BD=20=D1=81=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20User?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Employee.Delete (увольнение и soft-delete) и Employee.Update (деактивация) меняли только Employee.IsActive, но не трогали связанный AppUser. Логин и refresh в AuthorizationController гейтятся на User.IsActive — поэтому уволенный сотрудник продолжал входить и обновлять токены до 30 дней (ТЗ 0.4: «возможность залогиниться как удалённого сотрудника» = баг, P0). Добавлен SetLinkedUserActiveAsync: при деактивации сотрудника гасит User.IsActive и отзывает его valid OpenIddict-токены (как при удалении орг), при реактивации через Update — возвращает доступ. Вызывается из DELETE (оба шага) и из Update при смене активности. Найдено сценарием employees step07 (было: login/refresh уволенного → 200). Co-Authored-By: Claude Opus 4.7 --- .../Organizations/EmployeesController.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/food-market.api/Controllers/Organizations/EmployeesController.cs b/src/food-market.api/Controllers/Organizations/EmployeesController.cs index f309166..1b2f0b3 100644 --- a/src/food-market.api/Controllers/Organizations/EmployeesController.cs +++ b/src/food-market.api/Controllers/Organizations/EmployeesController.cs @@ -229,6 +229,9 @@ public async Task Update(Guid id, [FromBody] EmployeeInput input, var nowActive = input.IsActive; if (e.IsActive && !nowActive) e.FiredAt = DateTime.UtcNow; if (!e.IsActive && nowActive) e.FiredAt = null; + // Меняем активность сотрудника — синхронизируем логин связанного User + // (деактивация гасит сессии, реактивация возвращает доступ). См. DELETE. + if (e.IsActive != nowActive) await SetLinkedUserActiveAsync(e.UserId, nowActive, ct); e.IsActive = nowActive; // Replace assignments wholesale @@ -294,10 +297,35 @@ public async Task Delete(Guid id, CancellationToken ct) e.IsDeleted = true; e.DeletedAt = DateTime.UtcNow; } + // Увольнение и soft-delete обязаны гасить логин связанного User: иначе + // уволенный сотрудник продолжает входить и обновлять токены (ТЗ 0.4). + // На обоих шагах сотрудник перестаёт быть активным персоналом → гасим. + await DeactivateLinkedUserAsync(e.UserId, ct); await _db.SaveChangesAsync(ct); return NoContent(); } + /// Синхронизирует активность связанного AppUser с активностью + /// сотрудника. При деактивации дополнительно отзывает valid refresh/access + /// токены — без этого у уволенного остаётся рабочий refresh до 30 дней + /// (логин и refresh в AuthorizationController гейтятся на User.IsActive). + private async Task SetLinkedUserActiveAsync(Guid? userId, bool active, CancellationToken ct) + { + if (userId is null) return; + var user = await _db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId.Value, ct); + if (user is null || user.IsActive == active) return; + user.IsActive = active; + if (!active) + { + await _db.Database.ExecuteSqlRawAsync( + "UPDATE \"OpenIddictTokens\" SET \"Status\" = 'revoked' WHERE \"Subject\" = {0} AND \"Status\" = 'valid'", + user.Id.ToString()); + } + } + + private Task DeactivateLinkedUserAsync(Guid? userId, CancellationToken ct) + => SetLinkedUserActiveAsync(userId, active: false, ct); + private static Guid? ParseUserId(string? raw) => Guid.TryParse(raw, out var g) ? g : null; private async Task ProjectAsync(Guid id, CancellationToken ct)