811b6ef51a
Backend CI / build-and-test (push) Successful in 52s
Frontend CI / build-and-check (push) Failing after 5m15s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 16s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 1m0s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 32s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 13s
159 lines
5.7 KiB
C#
159 lines
5.7 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using UniVerse.Application.DTOs.Common;
|
|
using UniVerse.Application.DTOs.Users;
|
|
using UniVerse.Application.Interfaces;
|
|
using UniVerse.Application.Mappings;
|
|
using UniVerse.Domain.Enums;
|
|
using UniVerse.Domain.Exceptions;
|
|
using UniVerse.Infrastructure.Data;
|
|
|
|
namespace UniVerse.Infrastructure.Services;
|
|
|
|
public class UserService : IUserService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly IGamificationService _gamification;
|
|
|
|
public UserService(AppDbContext db, IGamificationService gamification)
|
|
{
|
|
_db = db;
|
|
_gamification = gamification;
|
|
}
|
|
|
|
public async Task<UserDto> GetByIdAsync(int id)
|
|
{
|
|
var user = await _db.Users
|
|
.Include(u => u.Roles)
|
|
.FirstOrDefaultAsync(u => u.Id == id)
|
|
?? throw new NotFoundException("User", id);
|
|
return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
|
|
}
|
|
|
|
public async Task<UserDto> UpdateProfileAsync(int id, UpdateUserRequest request)
|
|
{
|
|
var user = await _db.Users
|
|
.Include(u => u.Roles)
|
|
.FirstOrDefaultAsync(u => u.Id == id)
|
|
?? throw new NotFoundException("User", id);
|
|
|
|
if (request.DisplayName != null) user.DisplayName = request.DisplayName;
|
|
if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl;
|
|
user.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _db.SaveChangesAsync();
|
|
await _gamification.CheckAndAwardAchievementsAsync(id);
|
|
return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp));
|
|
}
|
|
|
|
public async Task<UserStatsDto> GetStatsAsync(int id)
|
|
{
|
|
var user = await _db.Users.FindAsync(id)
|
|
?? throw new NotFoundException("User", id);
|
|
|
|
var totalLectures = await _db.LectureEnrollments.CountAsync(e => e.UserId == id);
|
|
var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == id && e.Attended);
|
|
var reviews = await _db.Reviews.CountAsync(r => r.UserId == id);
|
|
var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id);
|
|
|
|
var level = await _gamification.CalculateLevelAsync(user.Xp);
|
|
var levelProgress = await _gamification.GetLevelProgressAsync(user.Xp);
|
|
|
|
return new UserStatsDto(
|
|
totalLectures, attended, reviews,
|
|
user.Xp, user.Coins, level, achievements,
|
|
levelProgress.CurrentLevelXp, levelProgress.NextLevelXp
|
|
);
|
|
}
|
|
|
|
public async Task<PagedResult<UserDto>> GetAllAsync(UserFilterRequest filter)
|
|
{
|
|
var query = _db.Users.AsQueryable();
|
|
|
|
if (!string.IsNullOrEmpty(filter.Search))
|
|
{
|
|
var search = filter.Search.ToLower();
|
|
query = query.Where(u =>
|
|
u.Email.ToLower().Contains(search) ||
|
|
(u.DisplayName != null && u.DisplayName.ToLower().Contains(search)));
|
|
}
|
|
|
|
query = query.Include(u => u.Roles);
|
|
|
|
if (filter.Role.HasValue)
|
|
{
|
|
var role = filter.Role.Value;
|
|
query = query.Where(u =>
|
|
u.Roles.Count == 1 &&
|
|
u.Roles.Any(ur => ur.Role == role));
|
|
}
|
|
|
|
if (filter.IsActive.HasValue)
|
|
query = query.Where(u => u.IsActive == filter.IsActive.Value);
|
|
|
|
var total = await query.CountAsync();
|
|
|
|
var users = await query
|
|
.OrderByDescending(u => u.CreatedAt)
|
|
.Skip((filter.Page - 1) * filter.PageSize)
|
|
.Take(filter.PageSize)
|
|
.ToListAsync();
|
|
|
|
var items = new List<UserDto>(users.Count);
|
|
foreach (var user in users)
|
|
items.Add(user.ToDto(await _gamification.CalculateLevelAsync(user.Xp)));
|
|
|
|
return PagedResult<UserDto>.Create(items, total, filter.Page, filter.PageSize);
|
|
}
|
|
|
|
public async Task SetRolesAsync(int id, IReadOnlyCollection<UserRole> roles)
|
|
{
|
|
var normalizedRoles = roles.Distinct().ToList();
|
|
if (normalizedRoles.Count == 0)
|
|
throw new ForbiddenException("At least one role is required.");
|
|
|
|
var user = await _db.Users
|
|
.Include(u => u.Roles)
|
|
.FirstOrDefaultAsync(u => u.Id == id)
|
|
?? throw new NotFoundException("User", id);
|
|
|
|
var existing = user.Roles.Select(r => r.Role).ToHashSet();
|
|
var toRemove = user.Roles.Where(r => !normalizedRoles.Contains(r.Role)).ToList();
|
|
foreach (var item in toRemove)
|
|
user.Roles.Remove(item);
|
|
|
|
var toAdd = normalizedRoles.Where(r => !existing.Contains(r)).ToList();
|
|
foreach (var role in toAdd)
|
|
user.Roles.Add(new Domain.Entities.UserRoleAssignment { UserId = user.Id, Role = role });
|
|
|
|
await EnsureProfilesForRolesAsync(user.Id, normalizedRoles);
|
|
user.UpdatedAt = DateTime.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task SetActiveAsync(int id, bool isActive)
|
|
{
|
|
var user = await _db.Users.FindAsync(id)
|
|
?? throw new NotFoundException("User", id);
|
|
user.IsActive = isActive;
|
|
user.UpdatedAt = DateTime.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task EnsureProfilesForRolesAsync(int userId, IReadOnlyCollection<UserRole> roles)
|
|
{
|
|
if (roles.Contains(UserRole.Student))
|
|
{
|
|
var hasStudentProfile = await _db.StudentProfiles.AnyAsync(p => p.UserId == userId);
|
|
if (!hasStudentProfile)
|
|
_db.StudentProfiles.Add(new Domain.Entities.StudentProfile { UserId = userId });
|
|
}
|
|
|
|
if (roles.Contains(UserRole.Teacher))
|
|
{
|
|
var hasTeacherProfile = await _db.TeacherProfiles.AnyAsync(p => p.UserId == userId);
|
|
if (!hasTeacherProfile)
|
|
_db.TeacherProfiles.Add(new Domain.Entities.TeacherProfile { UserId = userId });
|
|
}
|
|
}
|
|
}
|