/
afanasevn
/
MaxSystems
Обзор
Документация
Войти
/
afanasevn
/
MaxSystems
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/MaxSystems.Infrastructure/Services/UserService.cs
229 строк
8 KB
IBS\NAfanasev
Update documentation and comments
22 июн 2026, 19:13
22 июн 2026, 19:13
ff853a6
Код
Авторство
О чём код?
using MaxSystems.Application.Abstractions; using MaxSystems.Application.Contracts.Admin; using MaxSystems.Domain.Entities; using MaxSystems.Domain.Enums; using MaxSystems.Infrastructure.Audit; using MaxSystems.Infrastructure.Auth; using MaxSystems.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace MaxSystems.Infrastructure.Services; /// <summary>CRUD пользователей и справочник ролей портала.</summary> public sealed class UserService(AppDbContext db, IAuditService audit) : IUserService { private static readonly RoleInfoDto[] Roles = [ new(UserRole.Manager, "Главный менеджер", "Полный доступ, утверждение закупок, отчёты"), new(UserRole.LeadSpecialist, "Главный специалист", "Назначение работ, контроль ТО и качества"), new(UserRole.Specialist, "Специалист", "Выполнение работ, чек-листы, списание ЗИП"), new(UserRole.Warehouse, "Склад", "Приход, выдача, возврат, списание, инвентаризация"), new(UserRole.Dispatcher, "Диспетчер", "Просмотр статусов работ и связей с Service Desk"), new(UserRole.Contractor, "Подрядчик", "Ограниченный доступ к назначенным работам"), new(UserRole.Admin, "Администратор", "Справочники, роли, импорт, настройки") ]; /// <inheritdoc /> public async Task<IReadOnlyList<UserListItemDto>> ListAsync(CancellationToken ct = default) { return await db.Users .OrderBy(x => x.Role) .ThenBy(x => x.Name) .Select(x => new UserListItemDto( x.Id, x.Email, x.Name, x.Role, x.Active, x.LeadWorks.Count, x.AssignedWorks.Count)) .ToListAsync(ct); } /// <inheritdoc /> public async Task<UserListItemDto?> GetByIdAsync( Guid id, CancellationToken ct = default) { return await db.Users .Where(x => x.Id == id) .Select(x => new UserListItemDto( x.Id, x.Email, x.Name, x.Role, x.Active, x.LeadWorks.Count, x.AssignedWorks.Count)) .FirstOrDefaultAsync(ct); } /// <inheritdoc /> public async Task<(UserListItemDto? User, string? Error)> CreateAsync( CreateUserRequest request, Guid? actorUserId = null, CancellationToken ct = default) { var validationError = ValidateUserFields(request.Email, request.Name, request.Password, requirePassword: true); if (validationError is not null) return (null, validationError); var email = NormalizeEmail(request.Email); if (await db.Users.AnyAsync(x => x.Email == email, ct)) return (null, "Пользователь с таким email уже существует"); var user = new User { Id = Guid.NewGuid(), Email = email, Name = request.Name.Trim(), Role = request.Role, Active = request.Active, PasswordHash = PasswordHasherHelper.Hash(request.Password), CreatedAt = DateTime.UtcNow, }; db.Users.Add(user); audit.Log( actorUserId, "CREATE", "User", user.Id, AuditMessages.UserCreated(user.Name, user.Email, user.Role), new { user.Email, Role = user.Role.ToString(), user.Active }); await db.SaveChangesAsync(ct); return (await GetByIdAsync(user.Id, ct), null); } /// <inheritdoc /> public async Task<(UserListItemDto? User, string? Error)> UpdateAsync( Guid id, UpdateUserRequest request, Guid? currentUserId, CancellationToken ct = default) { var user = await db.Users.FirstOrDefaultAsync(x => x.Id == id, ct); if (user is null) return (null, null); var validationError = ValidateUserFields(request.Email, request.Name, request.Password, requirePassword: false); if (validationError is not null) return (null, validationError); if (currentUserId == id && !request.Active) return (null, "Нельзя отключить свою учётную запись"); if (user.Role == UserRole.Admin && request.Role != UserRole.Admin) { var otherActiveAdmins = await db.Users.CountAsync( x => x.Id != id && x.Role == UserRole.Admin && x.Active, ct); if (otherActiveAdmins == 0) return (null, "Нельзя снять роль администратора у последнего активного администратора"); } var email = NormalizeEmail(request.Email); if (await db.Users.AnyAsync(x => x.Id != id && x.Email == email, ct)) return (null, "Пользователь с таким email уже существует"); var passwordChanged = !string.IsNullOrWhiteSpace(request.Password); user.Email = email; user.Name = request.Name.Trim(); user.Role = request.Role; user.Active = request.Active; if (passwordChanged) user.PasswordHash = PasswordHasherHelper.Hash(request.Password); audit.Log( currentUserId, "UPDATE", "User", user.Id, AuditMessages.UserUpdated(user.Name, user.Email, passwordChanged), new { user.Email, Role = user.Role.ToString(), user.Active, PasswordChanged = passwordChanged }); await db.SaveChangesAsync(ct); return (await GetByIdAsync(id, ct), null); } /// <inheritdoc /> public async Task<(bool Deleted, string? Error)> DeleteAsync( Guid id, Guid? currentUserId, CancellationToken ct = default) { if (currentUserId == id) return (false, "Нельзя удалить свою учётную запись"); var user = await db.Users.FirstOrDefaultAsync(x => x.Id == id, ct); if (user is null) return (false, null); if (user.Role == UserRole.Admin && user.Active) { var otherActiveAdmins = await db.Users.CountAsync( x => x.Id != id && x.Role == UserRole.Admin && x.Active, ct); if (otherActiveAdmins == 0) return (false, "Нельзя удалить последнего активного администратора"); } var deletedName = user.Name; var deletedEmail = user.Email; db.Users.Remove(user); audit.Log( currentUserId, "DELETE", "User", id, AuditMessages.UserDeleted(deletedName, deletedEmail), new { Email = deletedEmail }); await db.SaveChangesAsync(ct); return (true, null); } /// <inheritdoc /> public IReadOnlyList<RoleInfoDto> GetRoles() => Roles; /// <summary>Проверяет обязательные поля и длину пароля.</summary> private static string? ValidateUserFields( string email, string name, string? password, bool requirePassword) { if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(name)) return "Укажите email и имя"; if (!email.Contains('@')) return "Некорректный email"; if (requirePassword) { if (string.IsNullOrWhiteSpace(password)) return "Укажите пароль"; if (password.Length < 6) return "Пароль должен быть не короче 6 символов"; } else if (!string.IsNullOrWhiteSpace(password) && password.Length < 6) { return "Пароль должен быть не короче 6 символов"; } return null; } /// <summary>Приводит email к нижнему регистру без пробелов по краям.</summary> private static string NormalizeEmail(string email) => email.Trim().ToLowerInvariant(); }