From 5d81bf9186a276d19a324d27a45764e4639c49b0 Mon Sep 17 00:00:00 2001 From: Seif Mohamed Date: Thu, 28 May 2026 00:59:50 +0300 Subject: [PATCH] Add contest registration and problem management features --- .../Controllers/Contests/ContestController.cs | 45 +++++++- .../Submissions/SubmissionsController.cs | 2 +- .../CurrentUser/ICurrentUserService.cs | 8 ++ .../ConfirmEmailCommandHandler.cs | 2 +- .../Register/RegisterUserCommandHandler.cs | 2 +- .../AddContestProblemCommand.cs | 7 ++ .../AddContestProblemCommandHandler.cs | 71 ++++++++++++ .../AddContestProblemResponse.cs | 5 + .../CreateContestCommandHandler.cs | 17 ++- .../GetContest/GetContestQueryHandler.cs | 103 ++++++++---------- .../RegisterInContestCommand.cs | 5 + .../RegisterInContestCommandHandler.cs | 51 +++++++++ .../RegisterInContestCommandValidator.cs | 11 ++ .../RegisterInContestResponse.cs | 3 + .../Mapping/ContestMappings.cs | 4 +- .../Mapping/ProblemMappings.cs | 2 +- .../Abstractions/IContestRepository.cs | 3 + .../Abstractions/IUserContestRepository.cs | 15 +++ .../Abstractions/IUserRepository.cs | 2 + src/CodeClash.Domain/Premitives/Helper.cs | 7 +- src/CodeClash.Domain/Premitives/Result.cs | 26 ++--- .../DependencyInjection.cs | 5 + .../Implementation/CurrentUserService.cs | 22 ++++ .../Repositories/ContestRepository.cs | 15 +++ .../Repositories/UserContestRepository.cs | 33 ++++++ .../Repositories/UserRepository.cs | 7 ++ 26 files changed, 388 insertions(+), 85 deletions(-) create mode 100644 src/CodeClash.Application/Abstractions/CurrentUser/ICurrentUserService.cs create mode 100644 src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommand.cs create mode 100644 src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommandHandler.cs create mode 100644 src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemResponse.cs create mode 100644 src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommand.cs create mode 100644 src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandHandler.cs create mode 100644 src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandValidator.cs create mode 100644 src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestResponse.cs create mode 100644 src/CodeClash.Domain/Abstractions/IUserContestRepository.cs create mode 100644 src/CodeClash.Infrastructure/Implementation/CurrentUserService.cs create mode 100644 src/CodeClash.Infrastructure/Repositories/UserContestRepository.cs diff --git a/src/CodeClash.API/Controllers/Contests/ContestController.cs b/src/CodeClash.API/Controllers/Contests/ContestController.cs index 06a549f..77886bf 100644 --- a/src/CodeClash.API/Controllers/Contests/ContestController.cs +++ b/src/CodeClash.API/Controllers/Contests/ContestController.cs @@ -1,5 +1,7 @@ -using CodeClash.Application.Contests.CreateContest; +using CodeClash.Application.Contests.AddContestProblems; +using CodeClash.Application.Contests.CreateContest; using CodeClash.Application.Contests.GetContest; +using CodeClash.Application.Contests.RegisterInContest; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -38,4 +40,45 @@ public async Task CreateContest( result) : BadRequest(result); } + + [Authorize] + [HttpPost("{contestId:guid}/register")] + public async Task RegisterInContest( + Guid contestId, + CancellationToken cancellationToken) + { + var result = await sender.Send(new RegisterInContestCommand(contestId), cancellationToken); + return result.IsSuccess + ? Ok(result) + : BadRequest(result); + } + + [HttpPost("{contestId:guid}/problems/{problemId:guid}")] + [Authorize] + public async Task AddProblem( + Guid contestId, + Guid problemId, + CancellationToken cancellationToken) + { + var result = await sender.Send( + new AddContestProblemCommand(contestId, problemId), + cancellationToken); + + if (result.IsSuccess) + { + return Ok(result); + } + + return result.Error!.Code switch + { + "Auth.Unauthorized" => Unauthorized(result), + "Contest.NotFound" => NotFound(result), + "Contest.Forbidden" => Forbid(), + "Contest.Locked" => BadRequest(result), + "Contest.ProblemLimitReached" => BadRequest(result), + "Problem.NotFound" => NotFound(result), + "Contest.DuplicateProblem" => Conflict(result), + _ => BadRequest(result) + }; + } } diff --git a/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs b/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs index 98f7027..f8cbdfe 100644 --- a/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs +++ b/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs @@ -27,7 +27,7 @@ public async Task GetProblemSubmissions(Guid problemId) return response.IsSuccess ? Ok(response.Value) - : response.Error.Code switch + : response.Error!.Code switch { "Auth.Error" => Forbid(), _ => NotFound() diff --git a/src/CodeClash.Application/Abstractions/CurrentUser/ICurrentUserService.cs b/src/CodeClash.Application/Abstractions/CurrentUser/ICurrentUserService.cs new file mode 100644 index 0000000..cff9d55 --- /dev/null +++ b/src/CodeClash.Application/Abstractions/CurrentUser/ICurrentUserService.cs @@ -0,0 +1,8 @@ +using CodeClash.Domain.Models.Identity; + +namespace CodeClash.Application.Abstractions.CurrentUser; +public interface ICurrentUserService +{ + string? IdentityId { get; } + Task GetUserAsync(); +} diff --git a/src/CodeClash.Application/Authentication/ConfirmEmail/ConfirmEmailCommandHandler.cs b/src/CodeClash.Application/Authentication/ConfirmEmail/ConfirmEmailCommandHandler.cs index 748efb4..d13d4a7 100644 --- a/src/CodeClash.Application/Authentication/ConfirmEmail/ConfirmEmailCommandHandler.cs +++ b/src/CodeClash.Application/Authentication/ConfirmEmail/ConfirmEmailCommandHandler.cs @@ -28,7 +28,7 @@ public async Task> Handle( if (confirmResult.IsFailure) { - return Result.Failure(confirmResult.Error); + return Result.Failure(confirmResult.Error!); } var email = confirmResult.Value; diff --git a/src/CodeClash.Application/Authentication/Register/RegisterUserCommandHandler.cs b/src/CodeClash.Application/Authentication/Register/RegisterUserCommandHandler.cs index 511e0c0..1d94737 100644 --- a/src/CodeClash.Application/Authentication/Register/RegisterUserCommandHandler.cs +++ b/src/CodeClash.Application/Authentication/Register/RegisterUserCommandHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle( if (identityResult.IsFailure) { - throw new Exception(identityResult.Error.Message); + throw new Exception(identityResult.Error!.Message); } var identityId = identityResult.Value; diff --git a/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommand.cs b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommand.cs new file mode 100644 index 0000000..4deca5a --- /dev/null +++ b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommand.cs @@ -0,0 +1,7 @@ +using CodeClash.Application.Abstractions.Messaging; + +namespace CodeClash.Application.Contests.AddContestProblems; +public sealed record AddContestProblemCommand( + Guid ContestId, + Guid ProblemId) : ICommand; + diff --git a/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommandHandler.cs b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommandHandler.cs new file mode 100644 index 0000000..d6c9d74 --- /dev/null +++ b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemCommandHandler.cs @@ -0,0 +1,71 @@ +using CodeClash.Application.Abstractions.CurrentUser; +using CodeClash.Application.Abstractions.Messaging; +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Premitives; + +namespace CodeClash.Application.Contests.AddContestProblems; +internal sealed class AddContestProblemCommandHandler( + IContestRepository contestRepository, + IProblemRepository problemRepository, + ICurrentUserService currentUserService, + IUnitOfWork unitOfWork) : ICommandHandler +{ + private const int MaxProblemsPerContest = 5; + public async Task> Handle( + AddContestProblemCommand request, + CancellationToken cancellationToken) + { + var user = await currentUserService.GetUserAsync(); + if (user is null) + { + return Result.Failure( + new Error("Auth.Unauthorized", "Unauthorized")); + } + + var contest = await contestRepository.GetByIdAsync(request.ContestId); + if (contest is null) + { + return Result.Failure( + new Error("Contest.NotFound", "Contest not found")); + } + + if (contest.SetterId != user.Id) + { + return Result.Failure( + new Error("Contest.Forbidden", "Only the contest creator can add problems")); + } + + if (contest.ContestStatus != ContestStatus.Upcoming) + { + return Result.Failure( + new Error("Contest.Locked", "Problems can only be added before the contest starts")); + } + + var currentProblemCount = await contestRepository.GetProblemCountAsync(request.ContestId); + if (currentProblemCount >= MaxProblemsPerContest) + { + return Result.Failure( + new Error("Contest.ProblemLimitReached", $"A contest cannot have more than {MaxProblemsPerContest} problems")); + } + var problem = await problemRepository.GetByIdAsync(request.ProblemId); + if (problem is null) + { + return Result.Failure( + new Error("Problem.NotFound", "Problem not found")); + } + + var alreadyExists = await contestRepository.HasProblemAsync(request.ContestId, request.ProblemId); + if (alreadyExists) + { + return Result.Failure( + new Error("Contest.DuplicateProblem", "This problem is already in the contest")); + } + + await contestRepository.AddProblemAsync(request.ContestId, request.ProblemId); + await unitOfWork.SaveChangesAsync(cancellationToken); + + var response = new AddContestProblemResponse(problem.Id, contest.Id, problem.Name); + + return Result.Success(response, "Problem added to contest successfully"); + } +} diff --git a/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemResponse.cs b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemResponse.cs new file mode 100644 index 0000000..06b9c53 --- /dev/null +++ b/src/CodeClash.Application/Contests/AddContestProblems/AddContestProblemResponse.cs @@ -0,0 +1,5 @@ +namespace CodeClash.Application.Contests.AddContestProblems; +public record AddContestProblemResponse( + Guid ContestId, + Guid ProblemId, + string ProblemName); diff --git a/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs index 1f5e489..3d16433 100644 --- a/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs +++ b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs @@ -1,26 +1,25 @@ -using System.Security.Claims; +using CodeClash.Application.Abstractions.CurrentUser; using CodeClash.Application.Abstractions.Messaging; using CodeClash.Application.Mapping; using CodeClash.Domain.Abstractions; using CodeClash.Domain.Premitives; -using Microsoft.AspNetCore.Http; namespace CodeClash.Application.Contests.CreateContest; internal sealed class CreateContestCommandHandler( IContestRepository contestRepository, - IUnitOfWork unitOfWork, - IHttpContextAccessor contextAccessor) + ICurrentUserService currentUserService, + IUnitOfWork unitOfWork) : ICommandHandler { public async Task> Handle( CreateContestCommand request, CancellationToken cancellationToken) { - var userId = contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); - - if (userId is null) + var user = await currentUserService.GetUserAsync(); + if (user is null) { - return Result.Failure(new Error("Auth.Error", "Unauthorized")); + return Result.Failure( + new Error("Auth.Unauthorized", "Unauthorized")); } if (request.StartTime >= request.EndTime) @@ -29,7 +28,7 @@ public async Task> Handle( new Error("Contest.InvalidDates", "Start time must be before end time")); } - var contest = request.ToContest(userId); + var contest = request.ToContest(user.Id); contestRepository.Add(contest); diff --git a/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs b/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs index f9836ee..bea67b4 100644 --- a/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs +++ b/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs @@ -1,95 +1,88 @@ -using System.Globalization; -using System.Text; -using CodeClash.Application.Abstractions.Cache; +using CodeClash.Application.Abstractions.Cache; +using CodeClash.Application.Abstractions.CurrentUser; using CodeClash.Application.Abstractions.Messaging; using CodeClash.Application.Mapping; using CodeClash.Domain.Abstractions; using CodeClash.Domain.Premitives; using CodeClash.Domain.Premitives.Responses; -using Microsoft.AspNetCore.Http; namespace CodeClash.Application.Contests.GetContest; internal sealed class GetContestQueryHandler( IResponseCacheService cacheService, - IHttpContextAccessor httpContext, - IContestRepository contestRepository) + IContestRepository contestRepository, + ICurrentUserService currentUserService, + IUserContestRepository userContestRepository) : IQueryHandler> { + private const int CacheDurationHours = 2; + public async Task>> Handle( GetContestQuery request, CancellationToken cancellationToken) { + var user = await currentUserService.GetUserAsync(); + if (user is null) + { + return Result.Failure>( + new Error("Auth.Unauthorized", "Unauthorized")); + } + var contest = await contestRepository.GetByIdAsync(request.Id); if (contest is null) { - return Result.Failure>(new Error("Contest.Not.Found", "Not Found!")); + return Result.Failure>( + new Error("Contest.NotFound", "Contest not found")); } - if (contest.ContestStatus == ContestStatus.Upcoming) + var isRegisterd = await userContestRepository.IsRegistered(request.Id, user.Id); + + if (!isRegisterd) { - return Result.Failure>(new Error("Contest.Not.Started", "Not Started yet!")); + return Result.Failure>( + new Error("Contest.NotRegistered", "You are not registered in this contest")); } if (contest.ContestStatus == ContestStatus.Running) { - string cacheKey = GenerateCacheKeyFromRequest(); - - // check cache - var cachedData = await cacheService.GetCachedResponseAsync(cacheKey); - - // cache hit → return cached data - if (cachedData is not null) - { - var serializedData = Helper.DeserializeCollection(cachedData); - - return Result.Success>( - serializedData.ToList(), - "Contest Problems fetched successfully"); - } - - // cache miss → get from db, cache it, return it - var problems = - await contestRepository.GetContestProblemsByIdAsync(request.Id); + return await HandleRunningContestAsync(request.Id); + } - var mappedResponse = problems - .Select(p => p.ToContestProblemResponse()) - .ToList(); + return await FetchProblemsAsync(request.Id); + } - await cacheService.CacheResponseAsync(cacheKey, mappedResponse, TimeSpan.FromHours(2)); + private async Task>> HandleRunningContestAsync( + Guid contestId) + { + string cacheKey = GenerateCacheKey(contestId); + var cachedData = await cacheService.GetCachedResponseAsync(cacheKey); + if (cachedData is not null) + { + var serialized = Helper.DeserializeCollection(cachedData); return Result.Success>( - mappedResponse, "Contest Problems fetched successfully"); + serialized.ToList(), "Contest Problems fetched successfully"); } - // past → get from db directly, no caching - var dbProblems = await contestRepository.GetContestProblemsByIdAsync(request.Id); - var response = dbProblems - .Select(p => p.ToContestProblemResponse()) - .ToList(); + var problems = await contestRepository.GetContestProblemsByIdAsync(contestId); + var mapped = problems.Select(p => p.ToContestProblemResponse()).ToList(); + + await cacheService.CacheResponseAsync(cacheKey, mapped, TimeSpan.FromHours(CacheDurationHours)); return Result.Success>( - response, "Contest Problems Feteched Successfully"); + mapped, "Contest Problems fetched successfully"); } - private string GenerateCacheKeyFromRequest() + private async Task>> FetchProblemsAsync( + Guid contestId) { - // key : unique for each request so generate it from request - // generate it from URL Path + Query String - var request = httpContext.HttpContext.Request; - var keyBuilder = new StringBuilder(); - - keyBuilder.Append(request.Path); - - // Ordered by key to handle cases when the order of query string - // parameters changes but the values remain the same. - // use InvariantCulture to avoid locale-dependent formatting warning + var problems = await contestRepository.GetContestProblemsByIdAsync(contestId); + var mapped = problems.Select(p => p.ToContestProblemResponse()).ToList(); - foreach (var (key, value) in request.Query.OrderBy(x => x.Key)) - { - keyBuilder.Append(CultureInfo.InvariantCulture, $"|{key}-{value}"); - } - - return keyBuilder.ToString(); + return Result.Success>( + mapped, "Contest Problems fetched successfully"); } + + private static string GenerateCacheKey(Guid contestId) => + $"contest-problems:{contestId}"; } diff --git a/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommand.cs b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommand.cs new file mode 100644 index 0000000..6777ca0 --- /dev/null +++ b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommand.cs @@ -0,0 +1,5 @@ +using CodeClash.Application.Abstractions.Messaging; + +namespace CodeClash.Application.Contests.RegisterInContest; +public record RegisterInContestCommand( + Guid Id) : ICommand; diff --git a/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandHandler.cs b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandHandler.cs new file mode 100644 index 0000000..25c5693 --- /dev/null +++ b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandHandler.cs @@ -0,0 +1,51 @@ +using CodeClash.Application.Abstractions.CurrentUser; +using CodeClash.Application.Abstractions.Messaging; +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Models.Contests; +using CodeClash.Domain.Premitives; + +namespace CodeClash.Application.Contests.RegisterInContest; +internal sealed class RegisterInContestCommandHandler( + IContestRepository contestRepository, + ICurrentUserService currentUserService, + IUserContestRepository userContestRepository) + : ICommandHandler +{ + public async Task> Handle( + RegisterInContestCommand request, + CancellationToken cancellationToken) + { + var user = await currentUserService.GetUserAsync(); + if (user is null) + { + return Result.Failure( + new Error("Auth.Unauthorized", "Unauthorized")); + } + + var contest = await contestRepository.GetByIdAsync(request.Id); + + if (contest is null) + { + return Result.Failure( + new Error("Contest.NotFound", "No contest found")); + } + + var isRegisterd = await userContestRepository.IsRegistered(request.Id, user.Id); + + if (isRegisterd) + { + return Result.Failure( + new Error("Contest.AlreadyRegistered", "Already registered in this contest")); + } + + var registration = new UserContest + { + UserId = user.Id, + ContestId = request.Id, + }; + + await userContestRepository.RegisterInContest(registration); + + return Result.Success(new RegisterInContestResponse(request.Id)); + } +} diff --git a/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandValidator.cs b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandValidator.cs new file mode 100644 index 0000000..397da0d --- /dev/null +++ b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestCommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace CodeClash.Application.Contests.RegisterInContest; +public class RegisterInContestCommandValidator + : AbstractValidator +{ + public RegisterInContestCommandValidator() + { + RuleFor(x => x.Id).NotNull().NotEmpty(); + } +} diff --git a/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestResponse.cs b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestResponse.cs new file mode 100644 index 0000000..9a2a38c --- /dev/null +++ b/src/CodeClash.Application/Contests/RegisterInContest/RegisterInContestResponse.cs @@ -0,0 +1,3 @@ +namespace CodeClash.Application.Contests.RegisterInContest; +public record RegisterInContestResponse( + Guid Id); diff --git a/src/CodeClash.Application/Mapping/ContestMappings.cs b/src/CodeClash.Application/Mapping/ContestMappings.cs index b0bda41..b363e54 100644 --- a/src/CodeClash.Application/Mapping/ContestMappings.cs +++ b/src/CodeClash.Application/Mapping/ContestMappings.cs @@ -9,8 +9,8 @@ public static Contest ToContest(this CreateContestCommand command, string userId { Name = command.Name, SetterId = userId, - StartDate = command.StartTime, - EndDate = command.EndTime, + StartDate = DateTime.SpecifyKind(command.StartTime, DateTimeKind.Utc), + EndDate = DateTime.SpecifyKind(command.EndTime, DateTimeKind.Utc), }; public static CreateContestResponse ToCreateContestResponse(this Contest contest) diff --git a/src/CodeClash.Application/Mapping/ProblemMappings.cs b/src/CodeClash.Application/Mapping/ProblemMappings.cs index e8b7d2b..daeee59 100644 --- a/src/CodeClash.Application/Mapping/ProblemMappings.cs +++ b/src/CodeClash.Application/Mapping/ProblemMappings.cs @@ -94,7 +94,7 @@ public static ContestProblemResponse ToContestProblemResponse(this Problem probl { return new ContestProblemResponse { - Id = problem.ContestId, + ContestId = problem.ContestId, Name = problem.Name, Difficulty = problem.Difficulty, RunTimeLimit = problem.RunTimeLimit, diff --git a/src/CodeClash.Domain/Abstractions/IContestRepository.cs b/src/CodeClash.Domain/Abstractions/IContestRepository.cs index 491fa1d..11dd713 100644 --- a/src/CodeClash.Domain/Abstractions/IContestRepository.cs +++ b/src/CodeClash.Domain/Abstractions/IContestRepository.cs @@ -5,4 +5,7 @@ namespace CodeClash.Domain.Abstractions; public interface IContestRepository : IGenericRepository { Task> GetContestProblemsByIdAsync(Guid contestId); + Task GetProblemCountAsync(Guid contestId); + Task HasProblemAsync(Guid contestId, Guid problemId); + Task AddProblemAsync(Guid contestId, Guid problemId); } diff --git a/src/CodeClash.Domain/Abstractions/IUserContestRepository.cs b/src/CodeClash.Domain/Abstractions/IUserContestRepository.cs new file mode 100644 index 0000000..27eb7dd --- /dev/null +++ b/src/CodeClash.Domain/Abstractions/IUserContestRepository.cs @@ -0,0 +1,15 @@ +using CodeClash.Domain.Models.Contests; + +namespace CodeClash.Domain.Abstractions; +public interface IUserContestRepository +{ + Task GetUserContest( + string userId, + Guid contestId); + + Task RegisterInContest(UserContest registration); + + Task IsRegistered( + Guid contestId, + string userId); +} diff --git a/src/CodeClash.Domain/Abstractions/IUserRepository.cs b/src/CodeClash.Domain/Abstractions/IUserRepository.cs index 188420b..e968df8 100644 --- a/src/CodeClash.Domain/Abstractions/IUserRepository.cs +++ b/src/CodeClash.Domain/Abstractions/IUserRepository.cs @@ -5,4 +5,6 @@ public interface IUserRepository { Task AddAsync(User user); + Task GetByIdentityIdAsync(string identityId); + } diff --git a/src/CodeClash.Domain/Premitives/Helper.cs b/src/CodeClash.Domain/Premitives/Helper.cs index a01155f..a5b335d 100644 --- a/src/CodeClash.Domain/Premitives/Helper.cs +++ b/src/CodeClash.Domain/Premitives/Helper.cs @@ -8,6 +8,10 @@ public static class Helper public const string PythonCompiler = "python:3.8-slim"; public const string CppCompiler = "gcc:latest"; public const string CSharpCompiler = "mcr.microsoft.com/dotnet/sdk:5.0"; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; public static T DeserializeObject(string json) { @@ -16,7 +20,8 @@ public static T DeserializeObject(string json) public static IEnumerable DeserializeCollection(string json) { - return JsonSerializer.Deserialize>(json); + return JsonSerializer.Deserialize>(json, JsonOptions) + ?? Enumerable.Empty(); } public static string Serialize(T obj) diff --git a/src/CodeClash.Domain/Premitives/Result.cs b/src/CodeClash.Domain/Premitives/Result.cs index 8673c24..45feeca 100644 --- a/src/CodeClash.Domain/Premitives/Result.cs +++ b/src/CodeClash.Domain/Premitives/Result.cs @@ -8,16 +8,16 @@ namespace CodeClash.Domain.Premitives; /// public class Result { - protected internal Result(bool isSuccess, Error error, string message = "") + protected internal Result(bool isSuccess, Error? error, string? message = null) { // Success must have Error.None - if (isSuccess && error != Error.None) + if (isSuccess && error is not null) { throw new InvalidOperationException(); } // Failure must have a real error - if (!isSuccess && error == Error.None) + if (!isSuccess && error is null) { throw new InvalidOperationException(); } @@ -35,27 +35,27 @@ protected internal Result(bool isSuccess, Error error, string message = "") public bool IsFailure => !IsSuccess; /// The error associated with a failure. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Error Error { get; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Error? Error { get; } - [JsonIgnore] - public string Message { get; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Message { get; } /// Create a successful result. public static Result Success(string message = "") - => new(true, Error.None, message); + => new(true, null, message); /// Create a failed result. public static Result Failure(Error error) - => new(false, error, error.Message); + => new(false, error, null); /// Create a successful result with a value. public static Result Success(TValue value, string message = "") - => new(value, true, Error.None, message); + => new(value, true, null, message); /// Create a failed result with a value type. public static Result Failure(Error error) - => new(default, false, error, error.Message); + => new(default, false, error, null); /// Create a result from a nullable value. public static Result Create(TValue? value) => @@ -72,8 +72,8 @@ public class Result : Result protected internal Result( TValue? value, bool isSuccess, - Error error, - string message = "") + Error? error, + string? message = null) : base(isSuccess, error, message) { _value = value; diff --git a/src/CodeClash.Infrastructure/DependencyInjection.cs b/src/CodeClash.Infrastructure/DependencyInjection.cs index 7206166..3dc9893 100644 --- a/src/CodeClash.Infrastructure/DependencyInjection.cs +++ b/src/CodeClash.Infrastructure/DependencyInjection.cs @@ -1,5 +1,6 @@ using System.Text; using CodeClash.Application.Abstractions.Cache; +using CodeClash.Application.Abstractions.CurrentUser; using CodeClash.Application.Abstractions.Data; using CodeClash.Application.Abstractions.ElasticSearch; using CodeClash.Application.Abstractions.Email; @@ -53,6 +54,10 @@ public static IServiceCollection AddInfrastructure( services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); diff --git a/src/CodeClash.Infrastructure/Implementation/CurrentUserService.cs b/src/CodeClash.Infrastructure/Implementation/CurrentUserService.cs new file mode 100644 index 0000000..e0cee0a --- /dev/null +++ b/src/CodeClash.Infrastructure/Implementation/CurrentUserService.cs @@ -0,0 +1,22 @@ +using System.Security.Claims; +using CodeClash.Application.Abstractions.CurrentUser; +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Models.Identity; +using Microsoft.AspNetCore.Http; + +namespace CodeClash.Infrastructure.Implementation; +internal sealed class CurrentUserService( + IHttpContextAccessor contextAccessor, + IUserRepository userRepository) : ICurrentUserService +{ + public string? IdentityId => + contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + + public Task GetUserAsync() + { + var identityId = IdentityId; + return identityId is null + ? Task.FromResult(null) + : userRepository.GetByIdentityIdAsync(identityId); + } +} diff --git a/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs b/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs index b12ba96..1966ee8 100644 --- a/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs +++ b/src/CodeClash.Infrastructure/Repositories/ContestRepository.cs @@ -13,6 +13,12 @@ public ContestRepository(ApplicationDbContext context) _context = context; } + public async Task GetProblemCountAsync(Guid contestId) => + await _context.Problems.CountAsync(p => p.ContestId == contestId); + + public async Task HasProblemAsync(Guid contestId, Guid problemId) => + await _context.Problems.AnyAsync(p => p.ContestId == contestId && p.Id == problemId); + public async Task> GetContestProblemsByIdAsync( Guid contestId) { @@ -21,4 +27,13 @@ public async Task> GetContestProblemsByIdAsync( return problems; } + + public async Task AddProblemAsync(Guid contestId, Guid problemId) + { + var problem = await _context.Problems.FindAsync(problemId); + if (problem is not null) + { + problem.ContestId = contestId; + } + } } diff --git a/src/CodeClash.Infrastructure/Repositories/UserContestRepository.cs b/src/CodeClash.Infrastructure/Repositories/UserContestRepository.cs new file mode 100644 index 0000000..95aefa6 --- /dev/null +++ b/src/CodeClash.Infrastructure/Repositories/UserContestRepository.cs @@ -0,0 +1,33 @@ +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Models.Contests; +using Microsoft.EntityFrameworkCore; + +namespace CodeClash.Infrastructure.Repositories; +internal sealed class UserContestRepository( + ApplicationDbContext context) : IUserContestRepository +{ + public async Task GetUserContest( + string userId, + Guid contestId) + { + return await context.Registers.FirstOrDefaultAsync( + r => r.UserId == userId + && r.ContestId == contestId); + } + + public async Task IsRegistered( + Guid contestId, + string userId) + { + return await context.Registers.AnyAsync( + r => r.UserId == userId + && r.ContestId == contestId); + } + + public async Task RegisterInContest(UserContest registration) + { + await context.Registers.AddAsync(registration); + await context.SaveChangesAsync(); + return true; + } +} diff --git a/src/CodeClash.Infrastructure/Repositories/UserRepository.cs b/src/CodeClash.Infrastructure/Repositories/UserRepository.cs index c681e4d..9e78b14 100644 --- a/src/CodeClash.Infrastructure/Repositories/UserRepository.cs +++ b/src/CodeClash.Infrastructure/Repositories/UserRepository.cs @@ -1,5 +1,6 @@ using CodeClash.Domain.Abstractions; using CodeClash.Domain.Models.Identity; +using Microsoft.EntityFrameworkCore; namespace CodeClash.Infrastructure.Repositories; internal sealed class UserRepository( @@ -9,4 +10,10 @@ public async Task AddAsync(User user) { await context.Users.AddAsync(user); } + + public async Task GetByIdentityIdAsync(string identityId) + { + return await context.Users + .FirstOrDefaultAsync(u => u.IdentityId == identityId); + } }