diff --git a/rag-2-backend/Config/ServiceRegistrationExtension.cs b/rag-2-backend/Config/ServiceRegistrationExtension.cs index de4a9ff..59a7f3d 100644 --- a/rag-2-backend/Config/ServiceRegistrationExtension.cs +++ b/rag-2-backend/Config/ServiceRegistrationExtension.cs @@ -7,6 +7,7 @@ using rag_2_backend.Infrastructure.Module.Administration; using rag_2_backend.Infrastructure.Module.Auth; using rag_2_backend.Infrastructure.Module.Background; +using rag_2_backend.Infrastructure.Module.Course; using rag_2_backend.Infrastructure.Module.Email; using rag_2_backend.Infrastructure.Module.Game; using rag_2_backend.Infrastructure.Module.GameRecord; @@ -52,6 +53,8 @@ private static void ConfigServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void ConfigSwagger(IServiceCollection services) diff --git a/rag-2-backend/Infrastructure/Common/Mapper/CourseMapper.cs b/rag-2-backend/Infrastructure/Common/Mapper/CourseMapper.cs new file mode 100644 index 0000000..3523002 --- /dev/null +++ b/rag-2-backend/Infrastructure/Common/Mapper/CourseMapper.cs @@ -0,0 +1,20 @@ +#region + +using rag_2_backend.Infrastructure.Database.Entity; +using rag_2_backend.Infrastructure.Module.Course.Dto; + +#endregion + +namespace rag_2_backend.Infrastructure.Common.Mapper; + +public static class CourseMapper +{ + public static CourseResponse Map(Course course) + { + return new CourseResponse + { + Id = course.Id, + Name = course.Name + }; + } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Common/Mapper/GameRecordMapper.cs b/rag-2-backend/Infrastructure/Common/Mapper/GameRecordMapper.cs index 53f99b4..dcf15f3 100644 --- a/rag-2-backend/Infrastructure/Common/Mapper/GameRecordMapper.cs +++ b/rag-2-backend/Infrastructure/Common/Mapper/GameRecordMapper.cs @@ -20,7 +20,9 @@ public static GameRecordResponse Map(GameRecord gameRecord) Started = gameRecord.Started, EndState = gameRecord.EndState, OutputSpec = gameRecord.OutputSpec, - SizeMb = gameRecord.SizeMb + SizeMb = gameRecord.SizeMb, + User = UserMapper.Map(gameRecord.User), + IsEmptyRecord = gameRecord.IsEmptyRecord }; } diff --git a/rag-2-backend/Infrastructure/Common/Mapper/UserMapper.cs b/rag-2-backend/Infrastructure/Common/Mapper/UserMapper.cs index 3a7f8ce..31ea202 100644 --- a/rag-2-backend/Infrastructure/Common/Mapper/UserMapper.cs +++ b/rag-2-backend/Infrastructure/Common/Mapper/UserMapper.cs @@ -16,11 +16,13 @@ public static UserResponse Map(User user) Id = user.Id, Email = user.Email, Role = user.Role, - StudyCycleYearA = user.StudyCycleYearA, - StudyCycleYearB = user.StudyCycleYearB, + StudyCycleYearA = user.StudyCycleYearA == 0 ? null : user.StudyCycleYearA, + StudyCycleYearB = user.StudyCycleYearB == 0 ? null : user.StudyCycleYearB, Name = user.Name, LastPlayed = user.LastPlayed.Equals(DateTime.MinValue) ? null : user.LastPlayed, - Banned = user.Banned + Banned = user.Banned, + Course = user.Course != null ? CourseMapper.Map(user.Course) : null, + Group = user.Group }; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Dao/CourseDao.cs b/rag-2-backend/Infrastructure/Dao/CourseDao.cs new file mode 100644 index 0000000..eaa6c91 --- /dev/null +++ b/rag-2-backend/Infrastructure/Dao/CourseDao.cs @@ -0,0 +1,23 @@ +#region + +using HttpExceptions.Exceptions; +using rag_2_backend.Infrastructure.Database; +using rag_2_backend.Infrastructure.Database.Entity; + +#endregion + +namespace rag_2_backend.Infrastructure.Dao; + +public class CourseDao(DatabaseContext dbContext) +{ + public virtual Course GetCourseByIdOrThrow(int id) + { + return dbContext.Courses.SingleOrDefault(u => u.Id == id) ?? + throw new NotFoundException("Course not found"); + } + + public virtual List GetAllCourses() + { + return dbContext.Courses.ToList(); + } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Dao/GameRecordDao.cs b/rag-2-backend/Infrastructure/Dao/GameRecordDao.cs index 6f51271..c409175 100644 --- a/rag-2-backend/Infrastructure/Dao/GameRecordDao.cs +++ b/rag-2-backend/Infrastructure/Dao/GameRecordDao.cs @@ -15,12 +15,12 @@ namespace rag_2_backend.Infrastructure.Dao; public class GameRecordDao(DatabaseContext dbContext) { - public virtual List GetRecordsByGameAndUser(int gameId, string email) + public virtual List GetRecordsByGameAndUser(int gameId, int userId) { return dbContext.GameRecords .Include(r => r.Game) .Include(r => r.User) - .Where(r => r.Game.Id == gameId && r.User.Email == email) + .Where(r => r.Game.Id == gameId && r.User.Id == userId) .ToList() .Select(GameRecordMapper.Map) .ToList(); @@ -72,7 +72,7 @@ public virtual void PerformGameRecordTransaction(Game game, GameRecord gameRecor try { dbContext.Database.ExecuteSqlRaw( - "SELECT InsertRecordedGame(@GameId, @Values, @UserId, @Players, @OutputSpec, @EndState, @Started, @Ended, @SizeMb)", + "SELECT InsertRecordedGame(@GameId, @Values, @UserId, @Players, @OutputSpec, @EndState, @Started, @Ended, @SizeMb, @IsEmptyRecord)", new NpgsqlParameter("@GameId", game.Id), new NpgsqlParameter("@Values", JsonSerializer.Serialize(gameRecord.Values)), new NpgsqlParameter("@UserId", user.Id), @@ -81,7 +81,8 @@ public virtual void PerformGameRecordTransaction(Game game, GameRecord gameRecor new NpgsqlParameter("@EndState", gameRecord.EndState), new NpgsqlParameter("@Started", gameRecord.Started), new NpgsqlParameter("@Ended", gameRecord.Ended), - new NpgsqlParameter("@SizeMb", gameRecord.SizeMb) + new NpgsqlParameter("@SizeMb", gameRecord.SizeMb), + new NpgsqlParameter("@IsEmptyRecord", gameRecord.IsEmptyRecord) ); transaction.Commit(); } diff --git a/rag-2-backend/Infrastructure/Dao/RefreshTokenDao.cs b/rag-2-backend/Infrastructure/Dao/RefreshTokenDao.cs index 9306f4f..a7f11c6 100644 --- a/rag-2-backend/Infrastructure/Dao/RefreshTokenDao.cs +++ b/rag-2-backend/Infrastructure/Dao/RefreshTokenDao.cs @@ -15,4 +15,11 @@ public virtual void RemoveTokensForUser(User user) context.RefreshTokens.RemoveRange(unusedTokens); context.SaveChanges(); } + + public virtual void RemoveTokenByToken(string token) + { + var unusedTokens = context.RefreshTokens.Where(r => r.Token == token).ToList(); + context.RefreshTokens.RemoveRange(unusedTokens); + context.SaveChanges(); + } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Dao/UserDao.cs b/rag-2-backend/Infrastructure/Dao/UserDao.cs index d947959..70c29f1 100644 --- a/rag-2-backend/Infrastructure/Dao/UserDao.cs +++ b/rag-2-backend/Infrastructure/Dao/UserDao.cs @@ -1,6 +1,7 @@ #region using HttpExceptions.Exceptions; +using Microsoft.EntityFrameworkCore; using rag_2_backend.Infrastructure.Database; using rag_2_backend.Infrastructure.Database.Entity; @@ -18,7 +19,9 @@ public virtual User GetUserByIdOrThrow(int id) public virtual User GetUserByEmailOrThrow(string email) { - return context.Users.SingleOrDefault(u => u.Email == email) ?? + return context.Users + .Include(u => u.Course) + .SingleOrDefault(u => u.Email == email) ?? throw new NotFoundException("User not found"); } diff --git a/rag-2-backend/Infrastructure/Database/DatabaseContext.cs b/rag-2-backend/Infrastructure/Database/DatabaseContext.cs index 778de35..d82925b 100644 --- a/rag-2-backend/Infrastructure/Database/DatabaseContext.cs +++ b/rag-2-backend/Infrastructure/Database/DatabaseContext.cs @@ -18,6 +18,7 @@ public class DatabaseContext(DbContextOptions options) : DbCont public virtual required DbSet AccountConfirmationTokens { get; init; } public virtual required DbSet RefreshTokens { get; init; } public virtual required DbSet PasswordResetTokens { get; init; } + public virtual DbSet Courses { get; init; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/rag-2-backend/Infrastructure/Database/Entity/Course.cs b/rag-2-backend/Infrastructure/Database/Entity/Course.cs new file mode 100644 index 0000000..3d13c92 --- /dev/null +++ b/rag-2-backend/Infrastructure/Database/Entity/Course.cs @@ -0,0 +1,17 @@ +#region + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +#endregion + +namespace rag_2_backend.Infrastructure.Database.Entity; + +[Table("course_table")] +[Index(nameof(Name), IsUnique = true)] +public class Course +{ + [Key] public int Id { get; init; } + [MaxLength(100)] public string Name { get; set; } = ""; +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Database/Entity/GameRecord.cs b/rag-2-backend/Infrastructure/Database/Entity/GameRecord.cs index 4374dfb..58ffab7 100644 --- a/rag-2-backend/Infrastructure/Database/Entity/GameRecord.cs +++ b/rag-2-backend/Infrastructure/Database/Entity/GameRecord.cs @@ -21,4 +21,5 @@ public class GameRecord [MaxLength(1000)] public string? OutputSpec { get; init; } [MaxLength(500)] public string? EndState { get; init; } public double SizeMb { get; init; } + public bool IsEmptyRecord { get; init; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Database/Entity/User.cs b/rag-2-backend/Infrastructure/Database/Entity/User.cs index 243a765..a48a531 100644 --- a/rag-2-backend/Infrastructure/Database/Entity/User.cs +++ b/rag-2-backend/Infrastructure/Database/Entity/User.cs @@ -20,7 +20,15 @@ public User() //for ef public User(string email) { - var domain = email.Split('@')[1]; + string domain; + try + { + domain = email.Split('@')[1]; + } + catch (Exception) + { + throw new BadRequestException("Invalid email address"); + } if (!domain.Equals("stud.prz.edu.pl") && !domain.Equals("prz.edu.pl")) throw new BadRequestException("Wrong domain"); @@ -32,11 +40,13 @@ public User(string email) [Key] public int Id { get; init; } [MaxLength(100)] public string Email { get; init; } = ""; [MaxLength(100)] public required string Password { get; set; } - [MaxLength(100)] public required string Name { get; init; } + [MaxLength(100)] public required string Name { get; set; } public Role Role { get; set; } public bool Confirmed { get; set; } public int StudyCycleYearA { get; set; } public int StudyCycleYearB { get; set; } public bool Banned { get; set; } public DateTime LastPlayed { get; set; } + public Course? Course { get; set; } + [MaxLength(100)] public string? Group { get; set; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Administration/AdministrationController.cs b/rag-2-backend/Infrastructure/Module/Administration/AdministrationController.cs index 27e2651..c78c534 100644 --- a/rag-2-backend/Infrastructure/Module/Administration/AdministrationController.cs +++ b/rag-2-backend/Infrastructure/Module/Administration/AdministrationController.cs @@ -38,10 +38,10 @@ public void ChangeRole([Required] int userId, [Required] Role role) administrationService.ChangeRole(userId, role); } - /// Get details of any user by user ID, only yours if not admin or teacher (Auth) + /// Get details of any user by user ID (Admin, Teacher) /// Cannot view details [HttpGet("{userId:int}/details")] - [Authorize] + [Authorize(Roles = "Admin,Teacher")] public UserResponse GetUserDetails([Required] int userId) { return administrationService.GetUserDetails(AuthDao.GetPrincipalEmail(User), userId); diff --git a/rag-2-backend/Infrastructure/Module/Auth/AuthController.cs b/rag-2-backend/Infrastructure/Module/Auth/AuthController.cs index fea3688..644c1f1 100644 --- a/rag-2-backend/Infrastructure/Module/Auth/AuthController.cs +++ b/rag-2-backend/Infrastructure/Module/Auth/AuthController.cs @@ -29,10 +29,14 @@ public void VerifyToken() [HttpPost("login")] public string Login([FromBody] [Required] UserLoginRequest loginRequest) { + var refreshTokenExpiryDays = loginRequest.RememberMe + ? double.Parse(config["RefreshToken:ExpireDaysRememberMe"] ?? "10") + : double.Parse(config["RefreshToken:ExpireDays"] ?? "1"); + var response = authService.LoginUser( loginRequest.Email, loginRequest.Password, - double.Parse(config["RefreshToken:ExpireDays"] ?? "30") + refreshTokenExpiryDays ); HttpContext.Response.Cookies.Append("refreshToken", response.RefreshToken, @@ -65,7 +69,10 @@ public string RefreshToken() [Authorize] public void Logout() { - authService.LogoutUser(AuthDao.GetPrincipalEmail(User)); + HttpContext.Request.Cookies.TryGetValue("refreshToken", out var refreshToken); + + if (refreshToken != null) + authService.LogoutUser(refreshToken); } /// Get current user details (Auth) diff --git a/rag-2-backend/Infrastructure/Module/Auth/AuthService.cs b/rag-2-backend/Infrastructure/Module/Auth/AuthService.cs index ee60623..dc296ba 100644 --- a/rag-2-backend/Infrastructure/Module/Auth/AuthService.cs +++ b/rag-2-backend/Infrastructure/Module/Auth/AuthService.cs @@ -57,10 +57,9 @@ public UserResponse GetMe(string email) return UserMapper.Map(userDao.GetUserByEmailOrThrow(email)); } - public void LogoutUser(string email) + public void LogoutUser(string token) { - var user = userDao.GetUserByEmailOrThrow(email); - refreshTokenDao.RemoveTokensForUser(user); + refreshTokenDao.RemoveTokenByToken(token); } // @@ -73,7 +72,6 @@ private RefreshToken GenerateRefreshToken(double refreshTokenExpirationTimeDays, Expiration = DateTime.Now.AddDays(refreshTokenExpirationTimeDays), Token = Guid.NewGuid().ToString() }; - refreshTokenDao.RemoveTokensForUser(user); databaseContext.RefreshTokens.Add(refreshToken); databaseContext.SaveChanges(); return refreshToken; diff --git a/rag-2-backend/Infrastructure/Module/Auth/Dto/UserLoginRequest.cs b/rag-2-backend/Infrastructure/Module/Auth/Dto/UserLoginRequest.cs index c5c2a8d..1dc6b7e 100644 --- a/rag-2-backend/Infrastructure/Module/Auth/Dto/UserLoginRequest.cs +++ b/rag-2-backend/Infrastructure/Module/Auth/Dto/UserLoginRequest.cs @@ -4,4 +4,5 @@ public class UserLoginRequest { public required string Email { get; set; } public required string Password { get; set; } + public required bool RememberMe { get; set; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Course/CourseController.cs b/rag-2-backend/Infrastructure/Module/Course/CourseController.cs new file mode 100644 index 0000000..472df8b --- /dev/null +++ b/rag-2-backend/Infrastructure/Module/Course/CourseController.cs @@ -0,0 +1,51 @@ +#region + +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using rag_2_backend.Infrastructure.Module.Course.Dto; + +#endregion + +namespace rag_2_backend.Infrastructure.Module.Course; + +[ApiController] +[Route("api/[controller]")] +public class CourseController(CourseService courseService) : ControllerBase +{ + /// Get courses available in system + [HttpGet] + public async Task> GetCourses() + { + return await courseService.GetCourses(); + } + + /// Add new course to system (Admin) + /// Course with this name already exists + [HttpPost] + [Authorize(Roles = "Admin")] + public void Add([FromBody] [Required] CourseRequest request) + { + courseService.AddCourse(request); + } + + /// Edit existing course (Admin) + /// Course not found + /// Course with this name already exists + [HttpPut("{id:int}")] + [Authorize(Roles = "Admin")] + public void Edit([FromBody] [Required] CourseRequest request, int id) + { + courseService.EditCourse(request, id); + } + + /// Remove existing course (Admin) + /// Course not found + /// Cannot delete used course + [HttpDelete("{id:int}")] + [Authorize(Roles = "Admin")] + public void Remove([Required] int id) + { + courseService.RemoveCourse(id); + } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Course/CourseService.cs b/rag-2-backend/Infrastructure/Module/Course/CourseService.cs new file mode 100644 index 0000000..3f3e12a --- /dev/null +++ b/rag-2-backend/Infrastructure/Module/Course/CourseService.cs @@ -0,0 +1,63 @@ +#region + +using HttpExceptions.Exceptions; +using Microsoft.EntityFrameworkCore; +using rag_2_backend.Infrastructure.Common.Mapper; +using rag_2_backend.Infrastructure.Dao; +using rag_2_backend.Infrastructure.Database; +using rag_2_backend.Infrastructure.Module.Course.Dto; + +#endregion + +namespace rag_2_backend.Infrastructure.Module.Course; + +public class CourseService(DatabaseContext context, CourseDao courseDao) +{ + public async Task> GetCourses() + { + var courses = await context.Courses.ToListAsync(); + + return courses.Select(CourseMapper.Map); + } + + public void AddCourse(CourseRequest request) + { + if (context.Courses.Any(c => c.Name == request.Name)) + throw new BadRequestException("Course with this name already exists"); + + var course = new Database.Entity.Course + { + Name = request.Name + }; + + context.Courses.Add(course); + context.SaveChanges(); + } + + public void EditCourse(CourseRequest request, int id) + { + var course = courseDao.GetCourseByIdOrThrow(id); + + if (context.Courses.Any(c => c.Name == request.Name && c.Name != course.Name)) + throw new BadRequestException("Course with this name already exists"); + + course.Name = request.Name; + + context.Courses.Update(course); + context.SaveChanges(); + } + + public void RemoveCourse(int id) + { + var course = courseDao.GetCourseByIdOrThrow(id); + + var usersWithCourses = context.Users + .Include(u => u.Course).Count(u => u.Course != null && u.Course.Id == id); + + if (usersWithCourses > 0) + throw new BadRequestException("Cannot delete used course"); + + context.Courses.Remove(course); + context.SaveChanges(); + } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Course/Dto/CourseRequest.cs b/rag-2-backend/Infrastructure/Module/Course/Dto/CourseRequest.cs new file mode 100644 index 0000000..0dbcb00 --- /dev/null +++ b/rag-2-backend/Infrastructure/Module/Course/Dto/CourseRequest.cs @@ -0,0 +1,6 @@ +namespace rag_2_backend.Infrastructure.Module.Course.Dto; + +public class CourseRequest +{ + public required string Name { get; init; } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Course/Dto/CourseResponse.cs b/rag-2-backend/Infrastructure/Module/Course/Dto/CourseResponse.cs new file mode 100644 index 0000000..25b489c --- /dev/null +++ b/rag-2-backend/Infrastructure/Module/Course/Dto/CourseResponse.cs @@ -0,0 +1,7 @@ +namespace rag_2_backend.Infrastructure.Module.Course.Dto; + +public class CourseResponse +{ + public int Id { get; init; } + public required string Name { get; init; } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/Email/EmailService.cs b/rag-2-backend/Infrastructure/Module/Email/EmailService.cs index 3ab26d8..f06bbfd 100644 --- a/rag-2-backend/Infrastructure/Module/Email/EmailService.cs +++ b/rag-2-backend/Infrastructure/Module/Email/EmailService.cs @@ -11,20 +11,22 @@ public class EmailService(EmailSendingUtil emailSendingUtil, IConfiguration conf public virtual void SendConfirmationEmail(string to, string token) { var address = config.GetValue("FrontendURLs:MailConfirmationURL") + token; - var body = "Please confirm your email address by clicking this button: Confirm"; + var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "confirmation.html"); + var logoPath = Path.Combine(AppContext.BaseDirectory, "Templates/Images", "rag-2.png"); + var body = File.ReadAllText(templatePath).Replace("{{address}}", address); Task.Run(async () => - await emailSendingUtil.SendMail(to, "Confirmation email", body)); + await emailSendingUtil.SendMail(to, "Confirmation email", body, logoPath)); } public virtual void SendPasswordResetMail(string to, string token) { var address = config.GetValue("FrontendURLs:PasswordResetURL") + token; - var body = "Reset your password by clicking this button: Reset"; + var templatePath = Path.Combine(AppContext.BaseDirectory, "Templates", "password-reset.html"); + var logoPath = Path.Combine(AppContext.BaseDirectory, "Templates/Images", "rag-2.png"); + var body = File.ReadAllText(templatePath).Replace("{{address}}", address); Task.Run(async () => - await emailSendingUtil.SendMail(to, "Password reset", body)); + await emailSendingUtil.SendMail(to, "Password reset", body, logoPath)); } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordRequest.cs b/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordRequest.cs index ee117c5..f0b7d61 100644 --- a/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordRequest.cs +++ b/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordRequest.cs @@ -11,4 +11,5 @@ public class GameRecordRequest public required string GameName { get; init; } public required string OutputSpec { get; init; } public required List Values { get; init; } + public required List Players { get; init; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordResponse.cs b/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordResponse.cs index e05dc44..a792bcb 100644 --- a/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordResponse.cs +++ b/rag-2-backend/Infrastructure/Module/GameRecord/Dto/GameRecordResponse.cs @@ -1,6 +1,7 @@ #region using rag_2_backend.Infrastructure.Common.Model; +using rag_2_backend.Infrastructure.Module.User.Dto; #endregion @@ -10,10 +11,12 @@ public class GameRecordResponse { public int Id { get; set; } public required string GameName { get; set; } + public required UserResponse User { get; set; } public List? Players { get; set; } public DateTime Started { get; set; } public DateTime Ended { get; set; } public string? OutputSpec { get; set; } public object? EndState { get; set; } public double SizeMb { get; init; } + public bool IsEmptyRecord { get; init; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordController.cs b/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordController.cs index fe8909f..f169096 100644 --- a/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordController.cs +++ b/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordController.cs @@ -14,20 +14,21 @@ namespace rag_2_backend.Infrastructure.Module.GameRecord; [Route("api/[controller]")] public class GameRecordController(GameRecordService gameRecordService) : ControllerBase { - /// Get all recorded games for user by game ID and user (Auth) + /// Get all recorded games for user by game ID and user, admin and teacher can view everyone's data (Auth) /// User or game not found [HttpGet] [Authorize] - public List GetRecordsByGame([Required] int gameId) + public List GetRecordsByGame([Required] int gameId, [Required] int userId) { var email = AuthDao.GetPrincipalEmail(User); - return gameRecordService.GetRecordsByGameAndUser(gameId, email); + return gameRecordService.GetRecordsByGameAndUser(gameId, userId, email); } /// Download JSON file from specific game, admin and teacher can download everyone's data (Auth) /// User or game record not found /// Permission denied + /// Record is empty [HttpGet("{recordedGameId:int}")] [Authorize] public FileContentResult DownloadRecordData([Required] int recordedGameId) @@ -41,7 +42,7 @@ public FileContentResult DownloadRecordData([Required] int recordedGameId) /// Add game recording, limits are present (Auth) /// User or game not found - /// Space limit exceeded or values state cannot be empty + /// Space limit exceeded [HttpPost] [Authorize] public void AddGameRecord([FromBody] [Required] GameRecordRequest recordRequest) diff --git a/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordService.cs b/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordService.cs index 387f0be..79eebee 100644 --- a/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordService.cs +++ b/rag-2-backend/Infrastructure/Module/GameRecord/GameRecordService.cs @@ -23,9 +23,14 @@ public class GameRecordService( GameDao gameDao ) { - public List GetRecordsByGameAndUser(int gameId, string email) + public List GetRecordsByGameAndUser(int gameId, int userId, string email) { - return gameRecordDao.GetRecordsByGameAndUser(gameId, email); + var principal = userDao.GetUserByEmailOrThrow(email); + + if (principal.Id != userId && principal.Role.Equals(Role.Student)) + throw new BadRequestException("Permission denied"); + + return gameRecordDao.GetRecordsByGameAndUser(gameId, userId); } public byte[] DownloadRecordData(int recordedGameId, string email) @@ -34,18 +39,18 @@ public byte[] DownloadRecordData(int recordedGameId, string email) var recordedGame = gameRecordDao.GetRecordedGameById(recordedGameId); if (user.Id != recordedGame.User.Id && user.Role.Equals(Role.Student)) - throw new BadRequestException("Permission denied"); + throw new ForbiddenException("Permission denied"); + if (recordedGame.IsEmptyRecord) + throw new BadRequestException("Record is empty"); return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(GameRecordMapper.JsonMap(recordedGame))); } public void AddGameRecord(GameRecordRequest recordRequest, string email) { - if (recordRequest.Values.Count == 0 || recordRequest.Values[^1].State == null) - throw new BadRequestException("Value state cannot be empty"); - var user = userDao.GetUserByEmailOrThrow(email); - CheckUserDataLimit(recordRequest, user); + if (recordRequest.Values.Count > 0) + CheckUserDataLimit(recordRequest, user); var game = gameDao.GetGameByNameOrThrow(recordRequest.GameName); @@ -54,10 +59,13 @@ public void AddGameRecord(GameRecordRequest recordRequest, string email) Game = game, Values = recordRequest.Values, User = user, - Players = recordRequest.Values[0].Players, + Players = recordRequest.Players, OutputSpec = recordRequest.OutputSpec, - EndState = recordRequest.Values[^1].State?.ToString(), - SizeMb = JsonSerializer.Serialize(recordRequest.Values).Length / (1024.0 * 1024.0) + EndState = recordRequest.Values.Count > 0 ? recordRequest.Values[^1].State?.ToString() : "{}", + SizeMb = recordRequest.Values.Count > 0 + ? JsonSerializer.Serialize(recordRequest.Values).Length / (1024.0 * 1024.0) + : 0, + IsEmptyRecord = recordRequest.Values.Count == 0 }; UpdateTimestamps(recordRequest, recordedGame); @@ -115,8 +123,12 @@ private static void UpdateTimestamps(GameRecordRequest recordRequest, Database.E { try { - var startTimestamp = recordRequest.Values[0].Timestamp; - var endTimestamp = recordRequest.Values[^1].Timestamp; + var startTimestamp = recordRequest.Values.Count > 0 + ? recordRequest.Values[0].Timestamp + : DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + var endTimestamp = recordRequest.Values.Count > 0 + ? recordRequest.Values[^1].Timestamp + : DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); if (startTimestamp is not null) gameRecord.Started = DateTime.Parse(startTimestamp, null, DateTimeStyles.RoundtripKind); if (endTimestamp is not null) diff --git a/rag-2-backend/Infrastructure/Module/User/Dto/UserEditRequest.cs b/rag-2-backend/Infrastructure/Module/User/Dto/UserEditRequest.cs new file mode 100644 index 0000000..9a608fe --- /dev/null +++ b/rag-2-backend/Infrastructure/Module/User/Dto/UserEditRequest.cs @@ -0,0 +1,10 @@ +namespace rag_2_backend.Infrastructure.Module.User.Dto; + +public class UserEditRequest +{ + public required string Name { get; init; } + public int? StudyCycleYearA { get; init; } + public int? StudyCycleYearB { get; init; } + public int? CourseId { get; init; } + public string? Group { get; init; } +} \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/User/Dto/UserRequest.cs b/rag-2-backend/Infrastructure/Module/User/Dto/UserRequest.cs index 3f94a3a..ec63446 100644 --- a/rag-2-backend/Infrastructure/Module/User/Dto/UserRequest.cs +++ b/rag-2-backend/Infrastructure/Module/User/Dto/UserRequest.cs @@ -7,4 +7,6 @@ public class UserRequest public required string Name { get; init; } public int? StudyCycleYearA { get; init; } public int? StudyCycleYearB { get; init; } + public int? CourseId { get; init; } + public string? Group { get; init; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/User/Dto/UserResponse.cs b/rag-2-backend/Infrastructure/Module/User/Dto/UserResponse.cs index 5336c53..36b67ba 100644 --- a/rag-2-backend/Infrastructure/Module/User/Dto/UserResponse.cs +++ b/rag-2-backend/Infrastructure/Module/User/Dto/UserResponse.cs @@ -1,6 +1,7 @@ #region using rag_2_backend.Infrastructure.Common.Model; +using rag_2_backend.Infrastructure.Module.Course.Dto; #endregion @@ -12,8 +13,10 @@ public class UserResponse public required string Email { get; set; } public Role Role { get; set; } public required string Name { get; init; } - public required int StudyCycleYearA { get; set; } - public required int StudyCycleYearB { get; set; } + public required int? StudyCycleYearA { get; set; } + public required int? StudyCycleYearB { get; set; } public DateTime? LastPlayed { get; set; } public bool Banned { get; set; } + public CourseResponse? Course { get; set; } + public string? Group { get; set; } } \ No newline at end of file diff --git a/rag-2-backend/Infrastructure/Module/User/UserController.cs b/rag-2-backend/Infrastructure/Module/User/UserController.cs index 0326646..de639ca 100644 --- a/rag-2-backend/Infrastructure/Module/User/UserController.cs +++ b/rag-2-backend/Infrastructure/Module/User/UserController.cs @@ -15,7 +15,7 @@ namespace rag_2_backend.Infrastructure.Module.User; public class UserController(UserService userService) : ControllerBase { /// Register new user - /// User already exists or wrong study cycle year + /// User already exists or wrong data [HttpPost("register")] public void Register([FromBody] [Required] UserRequest userRequest) { @@ -39,6 +39,15 @@ public void ConfirmAccount([Required] string token) userService.ConfirmAccount(token); } + /// Edit account info (Auth) + /// Wrong data + [HttpPatch("update")] + [Authorize] + public void UpdateAccount([Required] UserEditRequest request) + { + userService.UpdateAccount(request, AuthDao.GetPrincipalEmail(User)); + } + /// Request password reset for given email [HttpPost("request-password-reset")] public void RequestPasswordReset([Required] string email) diff --git a/rag-2-backend/Infrastructure/Module/User/UserService.cs b/rag-2-backend/Infrastructure/Module/User/UserService.cs index abb97e7..4327e9e 100644 --- a/rag-2-backend/Infrastructure/Module/User/UserService.cs +++ b/rag-2-backend/Infrastructure/Module/User/UserService.cs @@ -18,7 +18,8 @@ public class UserService( DatabaseContext context, EmailService emailService, UserDao userDao, - RefreshTokenDao refreshTokenDao) + RefreshTokenDao refreshTokenDao, + CourseDao courseDao) { public void RegisterUser(UserRequest userRequest) { @@ -31,17 +32,36 @@ public void RegisterUser(UserRequest userRequest) Password = HashUtil.HashPassword(userRequest.Password) }; - if (user.Role == Role.Student && IsStudyYearWrong(userRequest)) - throw new BadRequestException("Wrong study cycle year"); - - user.StudyCycleYearA = userRequest.StudyCycleYearA ?? 0; - user.StudyCycleYearB = userRequest.StudyCycleYearB ?? 0; + UpdateUserProperties( + userRequest.StudyCycleYearA, + userRequest.StudyCycleYearB, + userRequest.CourseId, + userRequest.Group, + user + ); context.Users.Add(user); GenerateAccountTokenAndSendConfirmationMail(user); context.SaveChanges(); } + public void UpdateAccount(UserEditRequest request, string principalEmail) + { + var user = userDao.GetUserByEmailOrThrow(principalEmail); + + UpdateUserProperties( + request.StudyCycleYearA, + request.StudyCycleYearB, + request.CourseId, + request.Group, + user + ); + user.Name = request.Name; + + context.Users.Update(user); + context.SaveChanges(); + } + public void ResendConfirmationEmail(string email) { var user = userDao.GetUserByEmailOrThrow(email); @@ -133,11 +153,33 @@ public void DeleteAccount(string email, string header) // - private static bool IsStudyYearWrong(UserRequest userRequest) + private void UpdateUserProperties( + int? studyCycleYearA, int? studyCycleYearB, int? courseId, string? group, Database.Entity.User user + ) + { + if (user.Role == Role.Student && ( + !studyCycleYearA.HasValue || !studyCycleYearB.HasValue || !courseId.HasValue || + string.IsNullOrWhiteSpace(group)) + ) throw new BadRequestException("Invalid data"); + + if ((studyCycleYearA.HasValue && !studyCycleYearB.HasValue) || + (!studyCycleYearA.HasValue && studyCycleYearB.HasValue)) + throw new BadRequestException("Invalid data"); + + if (studyCycleYearA.HasValue && studyCycleYearB.HasValue && + !IsStudyCycleYearValid(studyCycleYearA.Value, studyCycleYearB.Value)) + throw new BadRequestException("Invalid data"); + + user.StudyCycleYearA = studyCycleYearA ?? 0; + user.StudyCycleYearB = studyCycleYearB ?? 0; + user.Course = courseId != null ? courseDao.GetCourseByIdOrThrow(courseId.Value) : null; + user.Group = group; + } + + private static bool IsStudyCycleYearValid(int studyCycleYearA, int studyCycleYearB) { - return !userRequest.StudyCycleYearA.HasValue || !userRequest.StudyCycleYearB.HasValue || - userRequest.StudyCycleYearA == 0 || userRequest.StudyCycleYearB == 0 || - userRequest.StudyCycleYearB - userRequest.StudyCycleYearA != 1; + return studyCycleYearA != 0 && studyCycleYearB != 0 && + studyCycleYearB - studyCycleYearA == 1; } private void GenerateAccountTokenAndSendConfirmationMail(Database.Entity.User user) diff --git a/rag-2-backend/Infrastructure/Util/EmailSendingUtil.cs b/rag-2-backend/Infrastructure/Util/EmailSendingUtil.cs index c4e62b1..cdef5f9 100644 --- a/rag-2-backend/Infrastructure/Util/EmailSendingUtil.cs +++ b/rag-2-backend/Infrastructure/Util/EmailSendingUtil.cs @@ -13,7 +13,7 @@ public class EmailSendingUtil(IOptions options) { private readonly MailSettings _mailSettings = options.Value; - public async Task SendMail(string to, string subject, string body) + public async Task SendMail(string to, string subject, string body, string? imagePath = null) { try { @@ -27,6 +27,12 @@ public async Task SendMail(string to, string subject, string body) { HtmlBody = body }; + if (imagePath != null && File.Exists(imagePath)) + { + var image = await emailBodyBuilder.LinkedResources.AddAsync(imagePath); + image.ContentId = "logo"; + } + emailMessage.Body = emailBodyBuilder.ToMessageBody(); using var mailClient = new SmtpClient(); @@ -37,8 +43,9 @@ public async Task SendMail(string to, string subject, string body) return true; } - catch (Exception) + catch (Exception e) { + Console.WriteLine(e); return false; } } diff --git a/rag-2-backend/Migrations/20241107192508_StoredProc.Designer.cs b/rag-2-backend/Migrations/20241107192509_StoredProc.Designer.cs similarity index 100% rename from rag-2-backend/Migrations/20241107192508_StoredProc.Designer.cs rename to rag-2-backend/Migrations/20241107192509_StoredProc.Designer.cs diff --git a/rag-2-backend/Migrations/20241107192508_StoredProc.cs b/rag-2-backend/Migrations/20241107192509_StoredProc.cs similarity index 84% rename from rag-2-backend/Migrations/20241107192508_StoredProc.cs rename to rag-2-backend/Migrations/20241107192509_StoredProc.cs index 318b57a..0f8ec4b 100644 --- a/rag-2-backend/Migrations/20241107192508_StoredProc.cs +++ b/rag-2-backend/Migrations/20241107192509_StoredProc.cs @@ -20,14 +20,15 @@ CREATE OR REPLACE FUNCTION InsertRecordedGame( p_end_state TEXT, p_started TIMESTAMP, p_ended TIMESTAMP, - p_sizeMb DOUBLE PRECISION + p_sizeMb DOUBLE PRECISION, + p_isEmptyRecord BOOLEAN ) RETURNS VOID AS $$ BEGIN --'Procedura składowana' - INSERT INTO ""game_record_table"" (""GameId"", ""Values"", ""UserId"", ""Players"", ""OutputSpec"", ""EndState"", ""Started"", ""Ended"", ""SizeMb"") - VALUES (p_game_id, p_values, p_user_id, p_players, p_output_spec, p_end_state, p_started, p_ended, p_sizeMb); + INSERT INTO ""game_record_table"" (""GameId"", ""Values"", ""UserId"", ""Players"", ""OutputSpec"", ""EndState"", ""Started"", ""Ended"", ""SizeMb"", ""IsEmptyRecord"") + VALUES (p_game_id, p_values, p_user_id, p_players, p_output_spec, p_end_state, p_started, p_ended, p_sizeMb, p_isEmptyRecord); UPDATE ""user_table"" SET ""LastPlayed"" = p_ended diff --git a/rag-2-backend/Migrations/20241117152243_CourseGroup.Designer.cs b/rag-2-backend/Migrations/20241117152243_CourseGroup.Designer.cs new file mode 100644 index 0000000..13acc11 --- /dev/null +++ b/rag-2-backend/Migrations/20241117152243_CourseGroup.Designer.cs @@ -0,0 +1,299 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using rag_2_backend.Infrastructure.Database; + +#nullable disable + +namespace rag_2_backend.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20241117152243_CourseGroup")] + partial class CourseGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.AccountConfirmationToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("account_confirmation_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("course_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("game_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.GameRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndState") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Ended") + .HasColumnType("timestamp without time zone"); + + b.Property("GameId") + .HasColumnType("integer"); + + b.Property("OutputSpec") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Players") + .HasColumnType("text"); + + b.Property("SizeMb") + .HasColumnType("double precision"); + + b.Property("Started") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Values") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("game_record_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.PasswordResetToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("password_reset_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.RefreshToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("refresh_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Banned") + .HasColumnType("boolean"); + + b.Property("Confirmed") + .HasColumnType("boolean"); + + b.Property("CourseId") + .HasColumnType("integer"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Group") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastPlayed") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("StudyCycleYearA") + .HasColumnType("integer"); + + b.Property("StudyCycleYearB") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("user_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.AccountConfirmationToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.GameRecord", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.Game", "Game") + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.PasswordResetToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.RefreshToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.User", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.Course", "Course") + .WithMany() + .HasForeignKey("CourseId"); + + b.Navigation("Course"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/rag-2-backend/Migrations/20241117152243_CourseGroup.cs b/rag-2-backend/Migrations/20241117152243_CourseGroup.cs new file mode 100644 index 0000000..2874060 --- /dev/null +++ b/rag-2-backend/Migrations/20241117152243_CourseGroup.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace rag_2_backend.Migrations +{ + /// + public partial class CourseGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CourseId", + table: "user_table", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "Group", + table: "user_table", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateTable( + name: "course_table", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_course_table", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_user_table_CourseId", + table: "user_table", + column: "CourseId"); + + migrationBuilder.CreateIndex( + name: "IX_course_table_Name", + table: "course_table", + column: "Name", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_user_table_course_table_CourseId", + table: "user_table", + column: "CourseId", + principalTable: "course_table", + principalColumn: "Id"); + + migrationBuilder.Sql(@" + INSERT INTO public.course_table (""Name"") + VALUES ('EF-DI') + ON CONFLICT DO NOTHING; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_user_table_course_table_CourseId", + table: "user_table"); + + migrationBuilder.DropTable( + name: "course_table"); + + migrationBuilder.DropIndex( + name: "IX_user_table_CourseId", + table: "user_table"); + + migrationBuilder.DropColumn( + name: "CourseId", + table: "user_table"); + + migrationBuilder.DropColumn( + name: "Group", + table: "user_table"); + + migrationBuilder.DropColumn( + name: "Description", + table: "game_table"); + } + } +} diff --git a/rag-2-backend/Migrations/20241119114456_EmptyRecord.Designer.cs b/rag-2-backend/Migrations/20241119114456_EmptyRecord.Designer.cs new file mode 100644 index 0000000..bf0a771 --- /dev/null +++ b/rag-2-backend/Migrations/20241119114456_EmptyRecord.Designer.cs @@ -0,0 +1,302 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using rag_2_backend.Infrastructure.Database; + +#nullable disable + +namespace rag_2_backend.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20241119114456_EmptyRecord")] + partial class EmptyRecord + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.AccountConfirmationToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("account_confirmation_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("course_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("game_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.GameRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndState") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Ended") + .HasColumnType("timestamp without time zone"); + + b.Property("GameId") + .HasColumnType("integer"); + + b.Property("IsEmptyRecord") + .HasColumnType("boolean"); + + b.Property("OutputSpec") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Players") + .HasColumnType("text"); + + b.Property("SizeMb") + .HasColumnType("double precision"); + + b.Property("Started") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Values") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId"); + + b.ToTable("game_record_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.PasswordResetToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("password_reset_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.RefreshToken", b => + { + b.Property("Token") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Expiration") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("refresh_token_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Banned") + .HasColumnType("boolean"); + + b.Property("Confirmed") + .HasColumnType("boolean"); + + b.Property("CourseId") + .HasColumnType("integer"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Group") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastPlayed") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("StudyCycleYearA") + .HasColumnType("integer"); + + b.Property("StudyCycleYearB") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("user_table"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.AccountConfirmationToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.GameRecord", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.Game", "Game") + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.PasswordResetToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.RefreshToken", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.User", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.Course", "Course") + .WithMany() + .HasForeignKey("CourseId"); + + b.Navigation("Course"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/rag-2-backend/Migrations/20241119114456_EmptyRecord.cs b/rag-2-backend/Migrations/20241119114456_EmptyRecord.cs new file mode 100644 index 0000000..919afae --- /dev/null +++ b/rag-2-backend/Migrations/20241119114456_EmptyRecord.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace rag_2_backend.Migrations +{ + /// + public partial class EmptyRecord : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsEmptyRecord", + table: "game_record_table", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsEmptyRecord", + table: "game_record_table"); + } + } +} diff --git a/rag-2-backend/Migrations/DatabaseContextModelSnapshot.cs b/rag-2-backend/Migrations/DatabaseContextModelSnapshot.cs index f98e816..df351d3 100644 --- a/rag-2-backend/Migrations/DatabaseContextModelSnapshot.cs +++ b/rag-2-backend/Migrations/DatabaseContextModelSnapshot.cs @@ -41,6 +41,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("account_confirmation_token_table"); }); + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("course_table"); + }); + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.Game", b => { b.Property("Id") @@ -49,6 +70,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + b.Property("Name") .IsRequired() .HasMaxLength(100) @@ -80,6 +105,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("GameId") .HasColumnType("integer"); + b.Property("IsEmptyRecord") + .HasColumnType("boolean"); + b.Property("OutputSpec") .HasMaxLength(1000) .HasColumnType("character varying(1000)"); @@ -161,11 +189,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Confirmed") .HasColumnType("boolean"); + b.Property("CourseId") + .HasColumnType("integer"); + b.Property("Email") .IsRequired() .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("Group") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("LastPlayed") .HasColumnType("timestamp without time zone"); @@ -189,7 +224,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.HasKey("Id"); - + + b.HasIndex("CourseId"); + b.HasIndex("Email") .IsUnique(); @@ -247,6 +284,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + + modelBuilder.Entity("rag_2_backend.Infrastructure.Database.Entity.User", b => + { + b.HasOne("rag_2_backend.Infrastructure.Database.Entity.Course", "Course") + .WithMany() + .HasForeignKey("CourseId"); + + b.Navigation("Course"); + }); #pragma warning restore 612, 618 } } diff --git a/rag-2-backend/Templates/Images/rag-2.png b/rag-2-backend/Templates/Images/rag-2.png new file mode 100644 index 0000000..e26166f Binary files /dev/null and b/rag-2-backend/Templates/Images/rag-2.png differ diff --git a/rag-2-backend/Templates/confirmation.html b/rag-2-backend/Templates/confirmation.html new file mode 100644 index 0000000..ddfb150 --- /dev/null +++ b/rag-2-backend/Templates/confirmation.html @@ -0,0 +1,95 @@ + + + + + + Confirm Registration + + + + + + + + + +
+

Dear User,

+

+ Thank you for registering in RAG-2!
+ Please confirm your account by clicking the link below: +

+

+ Confirm registration +

+

+ If you didn’t sign up on our page, you can ignore this email. +

+

Best regards,

+

RAG-2 team

+
+ + RAG-2 Logo + +
+

RUT-AI-GAMES 2.0

+

d.kalandyk@prz.edu.pl

+

Rzeszów University of Technology

+

Human-Computer Interaction Club "GEST"

+
+ + diff --git a/rag-2-backend/Templates/password-reset.html b/rag-2-backend/Templates/password-reset.html new file mode 100644 index 0000000..5c613a4 --- /dev/null +++ b/rag-2-backend/Templates/password-reset.html @@ -0,0 +1,95 @@ + + + + + + Password reset + + + + + + + + + +
+

Dear User,

+

+ This is your password reset email.
+ By clicking the following link, you will proceed to password reset page: +

+

+ Reset password +

+

+ If you didn’t request password change, do not click this link under any circumstances. +

+

Best regards,

+

RAG-2 team

+
+ + RAG-2 Logo + +
+

RUT-AI-GAMES 2.0

+

d.kalandyk@prz.edu.pl

+

Rzeszów University of Technology

+

Human-Computer Interaction Club "GEST"

+
+ + diff --git a/rag-2-backend/Test/Dao/CourseDaoTest.cs b/rag-2-backend/Test/Dao/CourseDaoTest.cs new file mode 100644 index 0000000..d5e65f8 --- /dev/null +++ b/rag-2-backend/Test/Dao/CourseDaoTest.cs @@ -0,0 +1,61 @@ +#region + +using HttpExceptions.Exceptions; +using Microsoft.EntityFrameworkCore; +using Moq; +using rag_2_backend.Infrastructure.Dao; +using rag_2_backend.Infrastructure.Database; +using rag_2_backend.Infrastructure.Database.Entity; +using Xunit; + +#endregion + +namespace rag_2_backend.Test.Dao; + +public class CourseDaoTest +{ + private readonly CourseDao _courseDao; + + private readonly Mock _dbContextMock = new( + new DbContextOptionsBuilder().Options + ); + + public CourseDaoTest() + { + _courseDao = new CourseDao(_dbContextMock.Object); + } + + private void SetUpCourses(IEnumerable courses) + { + var coursesQueryable = courses.AsQueryable(); + var coursesDbSetMock = new Mock>(); + coursesDbSetMock.As>().Setup(m => m.Provider).Returns(coursesQueryable.Provider); + coursesDbSetMock.As>().Setup(m => m.Expression).Returns(coursesQueryable.Expression); + coursesDbSetMock.As>().Setup(m => m.ElementType).Returns(coursesQueryable.ElementType); + using var enumerator = coursesQueryable.GetEnumerator(); + coursesDbSetMock.As>().Setup(m => m.GetEnumerator()).Returns(enumerator); + + _dbContextMock.Setup(db => db.Courses).Returns(coursesDbSetMock.Object); + } + + [Fact] + public void GetCourseById_ShouldReturnCourse() + { + var expectedCourse = new Course + { + Id = 1 + }; + SetUpCourses(new List { expectedCourse }); + var result = _courseDao.GetCourseByIdOrThrow(1); + + Assert.Equal(expectedCourse, result); + } + + [Fact] + public void GetCourseById_ShouldThrowNotFound() + { + SetUpCourses(new List()); + + Assert.Throws(() => _courseDao.GetCourseByIdOrThrow(2)); + } +} \ No newline at end of file diff --git a/rag-2-backend/Test/Dao/GameRecordDaoTest.cs b/rag-2-backend/Test/Dao/GameRecordDaoTest.cs index fa46506..fc84a40 100644 --- a/rag-2-backend/Test/Dao/GameRecordDaoTest.cs +++ b/rag-2-backend/Test/Dao/GameRecordDaoTest.cs @@ -51,6 +51,7 @@ public void GetRecordsByGameAndUser_ShouldReturnRecords_WhenRecordsExist() }; var user = new User { + Id = 1, Email = email, Password = null!, Name = null! @@ -63,7 +64,7 @@ public void GetRecordsByGameAndUser_ShouldReturnRecords_WhenRecordsExist() }; SetUpGameRecordsDbSet(new List { gameRecord }); - var result = _gameRecordDao.GetRecordsByGameAndUser(gameId, email); + var result = _gameRecordDao.GetRecordsByGameAndUser(gameId, 1); Assert.Single(result); } diff --git a/rag-2-backend/Test/Service/CourseServiceTest.cs b/rag-2-backend/Test/Service/CourseServiceTest.cs new file mode 100644 index 0000000..d8952f1 --- /dev/null +++ b/rag-2-backend/Test/Service/CourseServiceTest.cs @@ -0,0 +1,140 @@ +#region + +using HttpExceptions.Exceptions; +using Microsoft.EntityFrameworkCore; +using MockQueryable.Moq; +using Moq; +using Newtonsoft.Json; +using rag_2_backend.Infrastructure.Common.Mapper; +using rag_2_backend.Infrastructure.Dao; +using rag_2_backend.Infrastructure.Database; +using rag_2_backend.Infrastructure.Database.Entity; +using rag_2_backend.Infrastructure.Module.Course; +using rag_2_backend.Infrastructure.Module.Course.Dto; +using Xunit; + +#endregion + +namespace rag_2_backend.Test.Service; + +public class CourseServiceTest +{ + private readonly Mock _contextMock = new( + new DbContextOptionsBuilder().Options + ); + + private readonly List _courses = + [ + new() { Id = 1, Name = "Course1" }, + new() { Id = 2, Name = "Course2" } + ]; + + private readonly CourseService _courseService; + + public CourseServiceTest() + { + Mock courseDaoMock = new(_contextMock.Object); + _courseService = new CourseService(_contextMock.Object, courseDaoMock.Object); + courseDaoMock.Setup(dao => dao.GetCourseByIdOrThrow(It.IsAny())).Returns(_courses[0]); + _contextMock.Setup(c => c.Courses).Returns(_courses.AsQueryable().BuildMockDbSet().Object); + } + + [Fact] + public void ShouldGetAllCourses() + { + var actualCourses = _courseService.GetCourses().Result; + + Assert.Equal(JsonConvert.SerializeObject(_courses.Select(CourseMapper.Map)), + JsonConvert.SerializeObject(actualCourses)); + } + + [Fact] + public void ShouldAddCourse() + { + var courseRequest = new CourseRequest + { + Name = "Course3" + }; + + _courseService.AddCourse(courseRequest); + + _contextMock.Verify( + c => c.Courses.Add(It.Is(course => course.Name == courseRequest.Name)), + Times.Once); + } + + [Fact] + public void ShouldNotAddCourseIfCourseAlreadyExists() + { + var courseRequest = new CourseRequest + { + Name = "Course1" + }; + + Assert.Throws(() => _courseService.AddCourse(courseRequest)); + } + + [Fact] + public void ShouldRemoveCourse() + { + var users = new List + { + new() + { + Id = 1, + Name = "User1", + Password = null!, + Course = _courses[1] + } + }; + + _contextMock.Setup(c => c.Users).Returns(users.AsQueryable().BuildMockDbSet().Object); + _courseService.RemoveCourse(1); + + _contextMock.Verify(c => c.Courses.Remove(It.Is(course => course.Id == 1)), Times.Once); + } + + [Fact] + public void ShouldThrowWhenRemoveCourse() + { + var users = new List + { + new() + { + Id = 1, + Name = "User1", + Password = null!, + Course = _courses[0] + } + }; + _contextMock.Setup(c => c.Users).Returns(users.AsQueryable().BuildMockDbSet().Object); + + Assert.Throws(() => _courseService.RemoveCourse(1)); + } + + [Fact] + public void ShouldUpdateCourse() + { + var courseRequest = new CourseRequest + { + Name = "Course3" + }; + + _courseService.EditCourse(courseRequest, 1); + + _contextMock.Verify( + c => c.Courses.Update(It.Is(course => course.Name == courseRequest.Name)), + Times.Once); + } + + [Fact] + public void ShouldNotUpdateCourseIfCourseAlreadyExists() + { + var courseRequest = new CourseRequest + { + Name = "Course2" + }; + + Assert.Throws(() => _courseService.EditCourse(courseRequest, 1)); + } +} \ No newline at end of file diff --git a/rag-2-backend/Test/Service/GameRecordServiceTest.cs b/rag-2-backend/Test/Service/GameRecordServiceTest.cs index 7569a9a..ee9cddb 100644 --- a/rag-2-backend/Test/Service/GameRecordServiceTest.cs +++ b/rag-2-backend/Test/Service/GameRecordServiceTest.cs @@ -13,6 +13,7 @@ using rag_2_backend.Infrastructure.Database.Entity; using rag_2_backend.Infrastructure.Module.GameRecord; using rag_2_backend.Infrastructure.Module.GameRecord.Dto; +using rag_2_backend.Infrastructure.Module.User.Dto; using Xunit; #endregion @@ -54,12 +55,29 @@ public void GetRecordsByGameAndUser_ShouldReturnGameRecords() { new() { - GameName = null! + GameName = null!, + User = new UserResponse + { + Id = 0, + Email = null!, + Name = null!, + StudyCycleYearA = 0, + StudyCycleYearB = 0 + } } }; - _gameRecordDaoMock.Setup(dao => dao.GetRecordsByGameAndUser(gameId, email)).Returns(records); + var user = new User + { + Id = 1, + Email = email, + Role = Role.Teacher, + Password = null!, + Name = null! + }; + _userDaoMock.Setup(dao => dao.GetUserByEmailOrThrow(email)).Returns(user); + _gameRecordDaoMock.Setup(dao => dao.GetRecordsByGameAndUser(gameId, 1)).Returns(records); - var result = _gameRecordService.GetRecordsByGameAndUser(gameId, email); + var result = _gameRecordService.GetRecordsByGameAndUser(gameId, 1, email); Assert.Equal(records, result); } @@ -84,7 +102,7 @@ public void DownloadRecordData_ShouldReturnSerializedRecord() Game = new Game { Name = "pong" - }!, + }, Values = [] }; @@ -125,7 +143,7 @@ public void DownloadRecordData_ShouldThrowBadRequestException_WhenPermissionDeni _userDaoMock.Setup(dao => dao.GetUserByEmailOrThrow(email)).Returns(user); _gameRecordDaoMock.Setup(dao => dao.GetRecordedGameById(recordedGameId)).Returns(recordedGame); - Assert.Throws(() => _gameRecordService.DownloadRecordData(recordedGameId, email)); + Assert.Throws(() => _gameRecordService.DownloadRecordData(recordedGameId, email)); } [Fact] diff --git a/rag-2-backend/Test/Service/UserServiceTest.cs b/rag-2-backend/Test/Service/UserServiceTest.cs index ea0a06f..db9fdd3 100644 --- a/rag-2-backend/Test/Service/UserServiceTest.cs +++ b/rag-2-backend/Test/Service/UserServiceTest.cs @@ -37,7 +37,9 @@ public class UserServiceTest Name = "John", Password = HashUtil.HashPassword("password"), StudyCycleYearA = 2022, - StudyCycleYearB = 2023 + StudyCycleYearB = 2023, + Course = new Course(), + Group = "group" }; private readonly UserService _userService; @@ -59,12 +61,14 @@ public UserServiceTest() Mock userMock = new(_contextMock.Object); Mock refreshTokenDaoMock = new(_contextMock.Object); + Mock courseDaoMock = new(_contextMock.Object); userMock.Setup(u => u.GetUserByIdOrThrow(It.IsAny())).Returns(_user); userMock.Setup(u => u.GetUserByEmailOrThrow(It.IsAny())).Returns(_user); _userService = new UserService(_contextMock.Object, _emailService.Object, - userMock.Object, refreshTokenDaoMock.Object); + userMock.Object, refreshTokenDaoMock.Object, courseDaoMock.Object); + courseDaoMock.Setup(c => c.GetCourseByIdOrThrow(It.IsAny())).Returns(_user.Course); _contextMock.Setup(c => c.Users).Returns(() => new List { _user } .AsQueryable().BuildMockDbSet().Object); _contextMock.Setup(c => c.AccountConfirmationTokens) diff --git a/rag-2-backend/appsettings.Development.json b/rag-2-backend/appsettings.Development.json index 4517d60..0df88ac 100644 --- a/rag-2-backend/appsettings.Development.json +++ b/rag-2-backend/appsettings.Development.json @@ -20,7 +20,7 @@ "TeacherDataLimitMb": 40, "AdminDataLimitMb": 50, "MailSettings": { - "Password": "fzozhrthfueuuorm" + "Password": "tlmzlwgrgyubbhzl" }, "Redis": { "ConnectionString": "localhost:6379" diff --git a/rag-2-backend/appsettings.json b/rag-2-backend/appsettings.json index cd13410..f5fed37 100644 --- a/rag-2-backend/appsettings.json +++ b/rag-2-backend/appsettings.json @@ -8,10 +8,11 @@ "AllowedHosts": "*", "Jwt": { "Issuer": "immortalcoders.com", - "ExpireMinutes": 15 + "ExpireMinutes": 10 }, "RefreshToken": { - "ExpireDays": 30 + "ExpireDays": 1, + "ExpireDaysRememberMe": 10 }, "MailSettings": { "Host": "smtp.gmail.com", diff --git a/rag-2-backend/docker-compose.yml b/rag-2-backend/docker-compose.yml index 7130a00..ef27294 100644 --- a/rag-2-backend/docker-compose.yml +++ b/rag-2-backend/docker-compose.yml @@ -17,7 +17,6 @@ - "6379:6379" volumes: - redisdata:/data - restart: always volumes: pgdata: { } redisdata: { } \ No newline at end of file diff --git a/rag-2-backend/rag-2-backend.csproj b/rag-2-backend/rag-2-backend.csproj index ced6656..e080c49 100644 --- a/rag-2-backend/rag-2-backend.csproj +++ b/rag-2-backend/rag-2-backend.csproj @@ -46,4 +46,13 @@ + + + PreserveNewest + + + PreserveNewest + + +