using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Shared; using ROLAC.API.DTOs.Users; using ROLAC.API.Entities; namespace ROLAC.API.Services; public class UserManagementService : IUserManagementService { private readonly UserManager _userManager; private readonly AppDbContext _db; public UserManagementService(UserManager userManager, AppDbContext db) { _userManager = userManager; _db = db; } // ── GetPaged ───────────────────────────────────────────────────────────── public async Task> GetPagedAsync( int page, int pageSize, string? search) { var query = from u in _userManager.Users join m in _db.Members.IgnoreQueryFilters() on u.MemberId equals (int?)m.Id into mg from m in mg.DefaultIfEmpty() select new { u, m }; if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim().ToLower(); query = query.Where(x => x.u.Email!.ToLower().Contains(s) || (x.m != null && ( x.m.FirstName_en.ToLower().Contains(s) || x.m.LastName_en.ToLower().Contains(s) || (x.m.NickName != null && x.m.NickName.ToLower().Contains(s))))); } var total = await query.CountAsync(); var rows = await query .OrderBy(x => x.u.Email) .Skip((page - 1) * pageSize).Take(pageSize) .Select(x => new { x.u.Id, x.u.Email, x.u.MemberId, x.u.IsActive, x.u.LanguagePreference, x.u.LastLoginAt, x.u.CreatedAt, MemberDisplayName = x.m != null ? (x.m.NickName ?? x.m.FirstName_en) + " " + x.m.LastName_en : (string?)null, }) .ToListAsync(); // Batch-load roles var userIds = rows.Select(r => r.Id).ToList(); var roleMap = await ( from ur in _db.UserRoles join r in _db.Roles on ur.RoleId equals r.Id where userIds.Contains(ur.UserId) select new { ur.UserId, r.Name } ).ToListAsync(); var rolesByUser = roleMap .GroupBy(x => x.UserId) .ToDictionary(g => g.Key, g => g.Select(x => x.Name!).ToList()); var items = rows.Select(r => new UserListItemDto { Id = r.Id, Email = r.Email ?? "", MemberId = r.MemberId, MemberDisplayName = r.MemberDisplayName, IsActive = r.IsActive, LanguagePreference = r.LanguagePreference, LastLoginAt = r.LastLoginAt, CreatedAt = r.CreatedAt, Roles = rolesByUser.GetValueOrDefault(r.Id, []), }).ToList(); return new PagedResult { Items = items, TotalCount = total, Page = page, PageSize = pageSize }; } // ── GetById ────────────────────────────────────────────────────────────── public async Task GetByIdAsync(string id) { var user = await _userManager.FindByIdAsync(id); if (user is null) return null; var roles = await _userManager.GetRolesAsync(user); var memberName = user.MemberId.HasValue ? await _db.Members.IgnoreQueryFilters() .Where(m => m.Id == user.MemberId) .Select(m => (m.NickName ?? m.FirstName_en) + " " + m.LastName_en) .FirstOrDefaultAsync() : null; return new UserDto { Id = user.Id, Email = user.Email ?? "", MemberId = user.MemberId, MemberDisplayName = memberName, Roles = roles.ToList(), IsActive = user.IsActive, LanguagePreference = user.LanguagePreference, LastLoginAt = user.LastLoginAt, CreatedAt = user.CreatedAt, }; } // ── Create ─────────────────────────────────────────────────────────────── public async Task CreateAsync(CreateUserRequest request) { // Validate Member exists (FindAsync bypasses query filter — intentional) var member = await _db.Members.FindAsync(request.MemberId) ?? throw new InvalidOperationException( $"Member {request.MemberId} does not exist."); // One user per member if (_userManager.Users.Any(u => u.MemberId == request.MemberId)) throw new InvalidOperationException( "This member already has a user account."); // Unique email if (await _userManager.FindByEmailAsync(request.Email) is not null) throw new InvalidOperationException( $"Email '{request.Email}' is already in use."); var tempPassword = GenerateTempPassword(); var user = new AppUser { UserName = request.Email, Email = request.Email, EmailConfirmed = true, MemberId = request.MemberId, LanguagePreference = request.LanguagePreference, IsActive = true, CreatedAt = DateTime.UtcNow, }; var result = await _userManager.CreateAsync(user, tempPassword); if (!result.Succeeded) throw new InvalidOperationException( string.Join("; ", result.Errors.Select(e => e.Description))); await _userManager.AddToRolesAsync(user, request.Roles); return new CreateUserResult { UserId = user.Id, TempPassword = tempPassword }; } // ── Update ─────────────────────────────────────────────────────────────── public async Task UpdateAsync(string id, UpdateUserRequest request) { var user = await _userManager.FindByIdAsync(id) ?? throw new KeyNotFoundException($"User {id} not found."); user.Email = request.Email; user.UserName = request.Email; user.NormalizedEmail = request.Email.ToUpperInvariant(); user.NormalizedUserName = request.Email.ToUpperInvariant(); user.LanguagePreference = request.LanguagePreference; user.IsActive = request.IsActive; user.LockoutEnd = request.IsActive ? null : DateTimeOffset.MaxValue; var updateResult = await _userManager.UpdateAsync(user); if (!updateResult.Succeeded) throw new InvalidOperationException( string.Join("; ", updateResult.Errors.Select(e => e.Description))); var currentRoles = await _userManager.GetRolesAsync(user); var toRemove = currentRoles.Except(request.Roles).ToList(); var toAdd = request.Roles.Except(currentRoles).ToList(); if (toRemove.Count > 0) await _userManager.RemoveFromRolesAsync(user, toRemove); if (toAdd.Count > 0) await _userManager.AddToRolesAsync(user, toAdd); } // ── Deactivate ─────────────────────────────────────────────────────────── public async Task DeactivateAsync(string id) { var user = await _userManager.FindByIdAsync(id) ?? throw new KeyNotFoundException($"User {id} not found."); user.IsActive = false; user.LockoutEnd = DateTimeOffset.MaxValue; await _userManager.UpdateAsync(user); } // ── ResetPassword ──────────────────────────────────────────────────────── public async Task ResetPasswordAsync(string id) { var user = await _userManager.FindByIdAsync(id) ?? throw new KeyNotFoundException($"User {id} not found."); var tempPassword = GenerateTempPassword(); var token = await _userManager.GeneratePasswordResetTokenAsync(user); var result = await _userManager.ResetPasswordAsync(user, token, tempPassword); if (!result.Succeeded) throw new InvalidOperationException( string.Join("; ", result.Errors.Select(e => e.Description))); return tempPassword; } // ── Helpers ────────────────────────────────────────────────────────────── private static string GenerateTempPassword() { const string upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"; const string lower = "abcdefghjkmnpqrstuvwxyz"; const string digits = "23456789"; const string special = "!@#$%^"; const string all = upper + lower + digits + special; var rng = new Random(); var pw = new char[12]; pw[0] = upper[rng.Next(upper.Length)]; pw[1] = lower[rng.Next(lower.Length)]; pw[2] = digits[rng.Next(digits.Length)]; pw[3] = special[rng.Next(special.Length)]; for (var i = 4; i < 12; i++) pw[i] = all[rng.Next(all.Length)]; return new string(pw.OrderBy(_ => rng.Next()).ToArray()); } }