fix(employees): увольнение/деактивация гасит логин связанного User

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 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-26 11:47:56 +05:00
parent f2f64646b1
commit 5091d43f5d

View file

@ -229,6 +229,9 @@ public async Task<IActionResult> 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<IActionResult> 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();
}
/// <summary>Синхронизирует активность связанного AppUser с активностью
/// сотрудника. При деактивации дополнительно отзывает valid refresh/access
/// токены — без этого у уволенного остаётся рабочий refresh до 30 дней
/// (логин и refresh в AuthorizationController гейтятся на User.IsActive).</summary>
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<EmployeeDto?> ProjectAsync(Guid id, CancellationToken ct)