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:
parent
f2f64646b1
commit
5091d43f5d
|
|
@ -229,6 +229,9 @@ public async Task<IActionResult> Update(Guid id, [FromBody] EmployeeInput input,
|
||||||
var nowActive = input.IsActive;
|
var nowActive = input.IsActive;
|
||||||
if (e.IsActive && !nowActive) e.FiredAt = DateTime.UtcNow;
|
if (e.IsActive && !nowActive) e.FiredAt = DateTime.UtcNow;
|
||||||
if (!e.IsActive && nowActive) e.FiredAt = null;
|
if (!e.IsActive && nowActive) e.FiredAt = null;
|
||||||
|
// Меняем активность сотрудника — синхронизируем логин связанного User
|
||||||
|
// (деактивация гасит сессии, реактивация возвращает доступ). См. DELETE.
|
||||||
|
if (e.IsActive != nowActive) await SetLinkedUserActiveAsync(e.UserId, nowActive, ct);
|
||||||
e.IsActive = nowActive;
|
e.IsActive = nowActive;
|
||||||
|
|
||||||
// Replace assignments wholesale
|
// Replace assignments wholesale
|
||||||
|
|
@ -294,10 +297,35 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
e.IsDeleted = true;
|
e.IsDeleted = true;
|
||||||
e.DeletedAt = DateTime.UtcNow;
|
e.DeletedAt = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
// Увольнение и soft-delete обязаны гасить логин связанного User: иначе
|
||||||
|
// уволенный сотрудник продолжает входить и обновлять токены (ТЗ 0.4).
|
||||||
|
// На обоих шагах сотрудник перестаёт быть активным персоналом → гасим.
|
||||||
|
await DeactivateLinkedUserAsync(e.UserId, ct);
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
return NoContent();
|
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 static Guid? ParseUserId(string? raw) => Guid.TryParse(raw, out var g) ? g : null;
|
||||||
|
|
||||||
private async Task<EmployeeDto?> ProjectAsync(Guid id, CancellationToken ct)
|
private async Task<EmployeeDto?> ProjectAsync(Guid id, CancellationToken ct)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue