From d3b91d365587d6a1f8b23ff205efacf9988ca209 Mon Sep 17 00:00:00 2001 From: Seif Mohamed Date: Wed, 27 May 2026 16:38:31 +0300 Subject: [PATCH] Add contest creation, blog features, and refactor logic --- .../Controllers/Blogs/BlogsController.cs | 20 +++++++++ .../Controllers/Contests/ContestController.cs | 20 ++++++++- .../Problems/ProblemsController.cs | 11 +++-- .../Submissions/SubmissionsController.cs | 23 +++++++--- src/CodeClash.API/Program.cs | 2 +- .../Cache/IResponseCacheService.cs | 3 +- .../CreateContest/CreateContestCommand.cs | 8 ++++ .../CreateContestCommandHandler.cs | 40 +++++++++++++++++ .../CreateContestCommandValidator.cs | 14 ++++++ .../CreateContest/CreateContestResponse.cs | 4 ++ .../GetContest/GetContestQuery.cs | 2 +- .../GetContest/GetContestQueryHandler.cs | 9 ++-- .../GetContest/GetContestQueryValidator.cs | 2 +- .../GetContest/GetContestResponse.cs | 2 +- .../Mapping/ContestMappings.cs | 21 +++++++++ .../Mapping/ProblemMappings.cs | 10 ++++- .../GetAllProblem/GetAllProblemsQuery.cs | 10 ++--- .../GetAllProblemsQueryHandler.cs | 5 +-- .../GetAllProblemsQueryValidator.cs | 4 +- .../GetProblemSubmissionsQuery.cs | 4 +- .../GetProblemSubmissionsQueryHandler.cs | 27 ++---------- .../GetSubmissionDataQueryHandler.cs | 6 +-- .../Abstractions/IBlogRepository.cs | 16 +++++++ .../Abstractions/ISubmissionRepository.cs | 6 ++- src/CodeClash.Domain/Premitives/Helper.cs | 18 +++++++- .../Premitives/Responses/ProblemResponse.cs | 4 ++ .../DependencyInjection.cs | 2 + .../Implementation/ResponseCacheService.cs | 15 +------ .../Repositories/BlogRepository.cs | 33 ++++++++++++++ .../Repositories/SubmissionRepository.cs | 43 +++++++++++++++++-- 30 files changed, 301 insertions(+), 83 deletions(-) create mode 100644 src/CodeClash.API/Controllers/Blogs/BlogsController.cs create mode 100644 src/CodeClash.Application/Contests/CreateContest/CreateContestCommand.cs create mode 100644 src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs create mode 100644 src/CodeClash.Application/Contests/CreateContest/CreateContestCommandValidator.cs create mode 100644 src/CodeClash.Application/Contests/CreateContest/CreateContestResponse.cs rename src/CodeClash.Application/{Contest => Contests}/GetContest/GetContestQuery.cs (79%) rename src/CodeClash.Application/{Contest => Contests}/GetContest/GetContestQueryHandler.cs (93%) rename src/CodeClash.Application/{Contest => Contests}/GetContest/GetContestQueryValidator.cs (79%) rename src/CodeClash.Application/{Contest => Contests}/GetContest/GetContestResponse.cs (58%) create mode 100644 src/CodeClash.Application/Mapping/ContestMappings.cs create mode 100644 src/CodeClash.Domain/Abstractions/IBlogRepository.cs create mode 100644 src/CodeClash.Domain/Premitives/Responses/ProblemResponse.cs create mode 100644 src/CodeClash.Infrastructure/Repositories/BlogRepository.cs diff --git a/src/CodeClash.API/Controllers/Blogs/BlogsController.cs b/src/CodeClash.API/Controllers/Blogs/BlogsController.cs new file mode 100644 index 0000000..33699c6 --- /dev/null +++ b/src/CodeClash.API/Controllers/Blogs/BlogsController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CodeClash.API.Controllers.Blogs; +[Route("blogs")] +[ApiController] +public class BlogsController : ControllerBase +{ + //[HttpGet] + //[Authorize] + //public async Task>> GetProblemsForBlog() + //{ + // var problems = await blogRepository.GetProblemsForBlogAsync(); + + // var response = problems + // .Select(p => p.ToResponse()) + // .ToList(); + + // return Ok(response); + //} +} diff --git a/src/CodeClash.API/Controllers/Contests/ContestController.cs b/src/CodeClash.API/Controllers/Contests/ContestController.cs index 86a9a54..06a549f 100644 --- a/src/CodeClash.API/Controllers/Contests/ContestController.cs +++ b/src/CodeClash.API/Controllers/Contests/ContestController.cs @@ -1,5 +1,7 @@ -using CodeClash.Application.Contest.GetContest; +using CodeClash.Application.Contests.CreateContest; +using CodeClash.Application.Contests.GetContest; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace CodeClash.API.Controllers.Contests; @@ -20,4 +22,20 @@ public async Task GetContestProblems( ? Ok(result) : BadRequest(result); } + + [Authorize] + [HttpPost] + public async Task CreateContest( + CreateContestCommand command, + CancellationToken cancellationToken) + { + var result = await sender.Send(command, cancellationToken); + + return result.IsSuccess + ? CreatedAtAction( + nameof(GetContestProblems), + new { id = result.Value.Id }, + result) + : BadRequest(result); + } } diff --git a/src/CodeClash.API/Controllers/Problems/ProblemsController.cs b/src/CodeClash.API/Controllers/Problems/ProblemsController.cs index 7eb7204..7dcba65 100644 --- a/src/CodeClash.API/Controllers/Problems/ProblemsController.cs +++ b/src/CodeClash.API/Controllers/Problems/ProblemsController.cs @@ -45,7 +45,7 @@ public async Task Create( [HttpGet] [Authorize] public async Task GetProblems( - [FromQuery] List? topicsNames, + [FromQuery] List? topics, [FromQuery] string? problemName, [FromQuery] ProblemStatus? status, [FromQuery] Difficulty? difficulty, @@ -55,15 +55,14 @@ public async Task GetProblems( [FromQuery] int pageSize = 10) { var query = new GetAllProblemsQuery( - topicsNames, + topics, problemName, difficulty, - pageNumber, - pageSize, status, sortBy, - order - ); + order, + pageNumber, + pageSize); var result = await sender.Send(query); diff --git a/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs b/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs index f6be5a8..98f7027 100644 --- a/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs +++ b/src/CodeClash.API/Controllers/Submissions/SubmissionsController.cs @@ -1,4 +1,5 @@ -using CodeClash.Application.Submissions.GetProblemSubmissions; +using System.Security.Claims; +using CodeClash.Application.Submissions.GetProblemSubmissions; using CodeClash.Application.Submissions.GetSubmissionData; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -14,17 +15,29 @@ public class SubmissionsController( [HttpGet("problem/{problemId}")] public async Task GetProblemSubmissions(Guid problemId) { - var query = new GetProblemSubmissionsQuery(problemId); + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (userId is null) + { + return Unauthorized(); + } + + var query = new GetProblemSubmissionsQuery(problemId, userId); var response = await sender.Send(query); - return response.IsSuccess ? Ok(response.Value) : NotFound(); + return response.IsSuccess + ? Ok(response.Value) + : response.Error.Code switch + { + "Auth.Error" => Forbid(), + _ => NotFound() + }; } [HttpGet("{id}")] - public async Task GetSubmissionData(Guid submissionId) + public async Task GetSubmissionData(Guid id) { - var query = new GetSubmissionDataQuery(submissionId); + var query = new GetSubmissionDataQuery(id); var response = await sender.Send(query); diff --git a/src/CodeClash.API/Program.cs b/src/CodeClash.API/Program.cs index adf1044..0d8a40e 100644 --- a/src/CodeClash.API/Program.cs +++ b/src/CodeClash.API/Program.cs @@ -29,7 +29,7 @@ await app.ApplyMigrationsAsync(); - await app.InitializeElasticSearchAsync(); + //await app.InitializeElasticSearchAsync(); // await app.SeedDataAsync(); } diff --git a/src/CodeClash.Application/Abstractions/Cache/IResponseCacheService.cs b/src/CodeClash.Application/Abstractions/Cache/IResponseCacheService.cs index 420635e..fd67cc2 100644 --- a/src/CodeClash.Application/Abstractions/Cache/IResponseCacheService.cs +++ b/src/CodeClash.Application/Abstractions/Cache/IResponseCacheService.cs @@ -6,6 +6,5 @@ Task CacheResponseAsync( object Response, TimeSpan timeToLive); - Task> GetCachedResponseAsync( - string key) where T : class; + Task GetCachedResponseAsync(string key); } diff --git a/src/CodeClash.Application/Contests/CreateContest/CreateContestCommand.cs b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommand.cs new file mode 100644 index 0000000..d9b1a37 --- /dev/null +++ b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommand.cs @@ -0,0 +1,8 @@ +using CodeClash.Application.Abstractions.Messaging; + +namespace CodeClash.Application.Contests.CreateContest; +public sealed record CreateContestCommand( + string Name, + string Description, + DateTime StartTime, + DateTime EndTime) : ICommand; diff --git a/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs new file mode 100644 index 0000000..1f5e489 --- /dev/null +++ b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandHandler.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +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) + : ICommandHandler +{ + public async Task> Handle( + CreateContestCommand request, + CancellationToken cancellationToken) + { + var userId = contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (userId is null) + { + return Result.Failure(new Error("Auth.Error", "Unauthorized")); + } + + if (request.StartTime >= request.EndTime) + { + return Result.Failure( + new Error("Contest.InvalidDates", "Start time must be before end time")); + } + + var contest = request.ToContest(userId); + + contestRepository.Add(contest); + + await unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(contest.ToCreateContestResponse(), "Contest Created Successfully"); + } +} diff --git a/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandValidator.cs b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandValidator.cs new file mode 100644 index 0000000..747283b --- /dev/null +++ b/src/CodeClash.Application/Contests/CreateContest/CreateContestCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace CodeClash.Application.Contests.CreateContest; +public sealed class CreateContestCommandValidator + : AbstractValidator +{ + public CreateContestCommandValidator() + { + RuleFor(x => x.Name).NotEmpty().NotNull(); + RuleFor(x => x.Description).NotEmpty().NotNull(); + RuleFor(x => x.StartTime).NotEmpty().NotNull(); + RuleFor(x => x.EndTime).NotEmpty().NotNull(); + } +} diff --git a/src/CodeClash.Application/Contests/CreateContest/CreateContestResponse.cs b/src/CodeClash.Application/Contests/CreateContest/CreateContestResponse.cs new file mode 100644 index 0000000..c50ce5b --- /dev/null +++ b/src/CodeClash.Application/Contests/CreateContest/CreateContestResponse.cs @@ -0,0 +1,4 @@ +namespace CodeClash.Application.Contests.CreateContest; +public sealed record CreateContestResponse( + Guid Id, + string Name); diff --git a/src/CodeClash.Application/Contest/GetContest/GetContestQuery.cs b/src/CodeClash.Application/Contests/GetContest/GetContestQuery.cs similarity index 79% rename from src/CodeClash.Application/Contest/GetContest/GetContestQuery.cs rename to src/CodeClash.Application/Contests/GetContest/GetContestQuery.cs index a41a70e..b305573 100644 --- a/src/CodeClash.Application/Contest/GetContest/GetContestQuery.cs +++ b/src/CodeClash.Application/Contests/GetContest/GetContestQuery.cs @@ -1,6 +1,6 @@ using CodeClash.Application.Abstractions.Messaging; using CodeClash.Domain.Premitives.Responses; -namespace CodeClash.Application.Contest.GetContest; +namespace CodeClash.Application.Contests.GetContest; public sealed record GetContestQuery( Guid Id) : IQuery>; diff --git a/src/CodeClash.Application/Contest/GetContest/GetContestQueryHandler.cs b/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs similarity index 93% rename from src/CodeClash.Application/Contest/GetContest/GetContestQueryHandler.cs rename to src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs index dc9c0b1..f9836ee 100644 --- a/src/CodeClash.Application/Contest/GetContest/GetContestQueryHandler.cs +++ b/src/CodeClash.Application/Contests/GetContest/GetContestQueryHandler.cs @@ -8,7 +8,7 @@ using CodeClash.Domain.Premitives.Responses; using Microsoft.AspNetCore.Http; -namespace CodeClash.Application.Contest.GetContest; +namespace CodeClash.Application.Contests.GetContest; internal sealed class GetContestQueryHandler( IResponseCacheService cacheService, IHttpContextAccessor httpContext, @@ -36,14 +36,15 @@ public async Task>> Handle( string cacheKey = GenerateCacheKeyFromRequest(); // check cache - var cachedData = await cacheService.GetCachedResponseAsync( - cacheKey); + var cachedData = await cacheService.GetCachedResponseAsync(cacheKey); // cache hit → return cached data if (cachedData is not null) { + var serializedData = Helper.DeserializeCollection(cachedData); + return Result.Success>( - cachedData.ToList(), + serializedData.ToList(), "Contest Problems fetched successfully"); } diff --git a/src/CodeClash.Application/Contest/GetContest/GetContestQueryValidator.cs b/src/CodeClash.Application/Contests/GetContest/GetContestQueryValidator.cs similarity index 79% rename from src/CodeClash.Application/Contest/GetContest/GetContestQueryValidator.cs rename to src/CodeClash.Application/Contests/GetContest/GetContestQueryValidator.cs index e79f500..dcd6793 100644 --- a/src/CodeClash.Application/Contest/GetContest/GetContestQueryValidator.cs +++ b/src/CodeClash.Application/Contests/GetContest/GetContestQueryValidator.cs @@ -1,6 +1,6 @@ using FluentValidation; -namespace CodeClash.Application.Contest.GetContest; +namespace CodeClash.Application.Contests.GetContest; public sealed class GetContestQueryValidator : AbstractValidator { diff --git a/src/CodeClash.Application/Contest/GetContest/GetContestResponse.cs b/src/CodeClash.Application/Contests/GetContest/GetContestResponse.cs similarity index 58% rename from src/CodeClash.Application/Contest/GetContest/GetContestResponse.cs rename to src/CodeClash.Application/Contests/GetContest/GetContestResponse.cs index e4b5ecb..a2b16d3 100644 --- a/src/CodeClash.Application/Contest/GetContest/GetContestResponse.cs +++ b/src/CodeClash.Application/Contests/GetContest/GetContestResponse.cs @@ -1,4 +1,4 @@ -namespace CodeClash.Application.Contest.GetContest; +namespace CodeClash.Application.Contests.GetContest; internal sealed class GetContestResponse { public Guid Id { get; set; } diff --git a/src/CodeClash.Application/Mapping/ContestMappings.cs b/src/CodeClash.Application/Mapping/ContestMappings.cs new file mode 100644 index 0000000..b0bda41 --- /dev/null +++ b/src/CodeClash.Application/Mapping/ContestMappings.cs @@ -0,0 +1,21 @@ +using CodeClash.Application.Contests.CreateContest; +using CodeClash.Domain.Models.Contests; + +namespace CodeClash.Application.Mapping; +public static class ContestMappings +{ + public static Contest ToContest(this CreateContestCommand command, string userId) + => new() + { + Name = command.Name, + SetterId = userId, + StartDate = command.StartTime, + EndDate = command.EndTime, + }; + + public static CreateContestResponse ToCreateContestResponse(this Contest contest) + => new( + contest.Id, + contest.Name); + +} diff --git a/src/CodeClash.Application/Mapping/ProblemMappings.cs b/src/CodeClash.Application/Mapping/ProblemMappings.cs index bad69ae..e8b7d2b 100644 --- a/src/CodeClash.Application/Mapping/ProblemMappings.cs +++ b/src/CodeClash.Application/Mapping/ProblemMappings.cs @@ -37,6 +37,14 @@ public static CreateProblemResponse ToResponse(this Problem problem) problem.Difficulty.ToString()); } + public static ProblemResponse ToProblemResponse(this Problem problem) + { + return new ProblemResponse( + problem.Id, + problem.Name + ); + } + public static GetAllProblemResponse ToGetAllResponse(this ProblemDocument problem) { return new GetAllProblemResponse @@ -86,7 +94,7 @@ public static ContestProblemResponse ToContestProblemResponse(this Problem probl { return new ContestProblemResponse { - ContestId = problem.ContestId, + Id = problem.ContestId, Name = problem.Name, Difficulty = problem.Difficulty, RunTimeLimit = problem.RunTimeLimit, diff --git a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQuery.cs b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQuery.cs index ce9b18b..b8a3ea3 100644 --- a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQuery.cs +++ b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQuery.cs @@ -5,11 +5,11 @@ namespace CodeClash.Application.Problems.GetAll; public record GetAllProblemsQuery( - List? TopicsNames, + List? Topics, string? Name, Difficulty? Difficulty, - int PageNumber, - int PageSize, ProblemStatus? Status, - SortBy? SortBy, - Order? Order) : IQuery>; + SortBy SortBy, + Order Order, + int PageNumber = 1, + int PageSize = 10) : IQuery>; diff --git a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryHandler.cs b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryHandler.cs index 1ede053..3c65720 100644 --- a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryHandler.cs +++ b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryHandler.cs @@ -11,7 +11,6 @@ namespace CodeClash.Application.Problems.GetAllProblem; internal sealed class GetAllProblemsQueryHandler( IElasticService elasticService, ISubmissionRepository submissionRepository, - ITopicRepository topicRepository, IHttpContextAccessor contextAccessor) : IQueryHandler> { @@ -26,11 +25,9 @@ public async Task>> Handle( return Result.Failure>(new Error("Auth.Error", "Unauthorized")); } - var topicsIds = await topicRepository.GetTopicIDsByNamesAsync(request.TopicsNames!); - var (problemDocuments, totalPages) = await elasticService.SearchProblemsAsync( request.Name, - topicsIds, + request.Topics, request.Difficulty, request.SortBy, request.Order, diff --git a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryValidator.cs b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryValidator.cs index f7e8d54..a908ac2 100644 --- a/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryValidator.cs +++ b/src/CodeClash.Application/Problems/GetAllProblem/GetAllProblemsQueryValidator.cs @@ -23,9 +23,9 @@ public GetAllProblemsQueryValidator() .GreaterThanOrEqualTo(1) .WithMessage("Page size must be greater than or equal to 1."); - RuleFor(x => x.TopicsNames) + RuleFor(x => x.Topics) .Must(topicsNames => topicsNames == null || topicsNames.Count > 0) - .WithMessage("Topics names must be null or have at least one element."); + .WithMessage("Topics must be null or contain at least one id."); RuleFor(x => x.Name) .Must(problemName => problemName == null || problemName.Length > 0) diff --git a/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQuery.cs b/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQuery.cs index 86d593d..98bf0e9 100644 --- a/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQuery.cs +++ b/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQuery.cs @@ -2,5 +2,5 @@ namespace CodeClash.Application.Submissions.GetProblemSubmissions; public sealed record GetProblemSubmissionsQuery( - Guid ProblemId - ) : IQuery>; + Guid ProblemId, + string UserId) : IQuery>; diff --git a/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQueryHandler.cs b/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQueryHandler.cs index 9c4b8ff..0af7b9a 100644 --- a/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQueryHandler.cs +++ b/src/CodeClash.Application/Submissions/GetProblemSubmissions/GetProblemSubmissionsQueryHandler.cs @@ -1,40 +1,21 @@ -using System.Security.Claims; -using CodeClash.Application.Abstractions.Messaging; +using CodeClash.Application.Abstractions.Messaging; using CodeClash.Application.Mapping; using CodeClash.Domain.Abstractions; -using CodeClash.Domain.Models.Problems; using CodeClash.Domain.Models.Submits; using CodeClash.Domain.Premitives; -using Microsoft.AspNetCore.Http; namespace CodeClash.Application.Submissions.GetProblemSubmissions; internal sealed class GetProblemSubmissionsQueryHandler( - IProblemRepository problemRepository, - ISubmissionRepository submissionRepository, - IHttpContextAccessor contextAccessor) + ISubmissionRepository submissionRepository) : IQueryHandler> { public async Task>> Handle( GetProblemSubmissionsQuery request, CancellationToken cancellationToken) { - var userId = contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + var submissions = await submissionRepository.GetAllSubmissions(request.ProblemId, request.UserId); - if (userId is null) - { - return Result.Failure>(new Error("Auth.Error", "Unauthorized")); - } - - var problem = await problemRepository.GetByIdAsync(request.ProblemId); - - if (problem is null) - { - return Result.Failure>(ProblemErrors.NotFound); - } - - var submissions = submissionRepository.GetAllSubmissions(request.ProblemId, userId); - - if (submissions is null) + if (submissions is null || submissions.Count == 0) { return Result.Failure>(SubmitErrors.NotFound); } diff --git a/src/CodeClash.Application/Submissions/GetSubmissionData/GetSubmissionDataQueryHandler.cs b/src/CodeClash.Application/Submissions/GetSubmissionData/GetSubmissionDataQueryHandler.cs index dc2e94a..7dcbcfc 100644 --- a/src/CodeClash.Application/Submissions/GetSubmissionData/GetSubmissionDataQueryHandler.cs +++ b/src/CodeClash.Application/Submissions/GetSubmissionData/GetSubmissionDataQueryHandler.cs @@ -23,15 +23,13 @@ public async Task> Handle( return Result.Failure(new Error("Auth.Error", "Unauthorized")); } - var submission = await submissionRepository.GetByIdAsync(request.SubmissionId); + var submission = await submissionRepository.GetSubmissionIfAuthorized(userId, request.SubmissionId); if (submission is null) { return Result.Failure(SubmitErrors.NotFound); } - var mappedSub = submission.ToSubmit(); - - return Result.Success(mappedSub); + return Result.Success(submission.ToSubmit()); } } diff --git a/src/CodeClash.Domain/Abstractions/IBlogRepository.cs b/src/CodeClash.Domain/Abstractions/IBlogRepository.cs new file mode 100644 index 0000000..c05802a --- /dev/null +++ b/src/CodeClash.Domain/Abstractions/IBlogRepository.cs @@ -0,0 +1,16 @@ +using CodeClash.Domain.Models.Blogs; +using CodeClash.Domain.Models.Problems; + +namespace CodeClash.Domain.Abstractions; +public interface IBlogRepository +{ + // Problem methods + Task> GetProblemsForBlogAsync(); + Task AddProblemToBlogAsync(Guid blogId, Guid problemId); + + // Solution methods + Task AddSolutionToBlogAsync(Guid blogId, string solutionContent); + + // Get blogs by problem + Task> GetBlogsByProblemIdAsync(Guid problemId); +} diff --git a/src/CodeClash.Domain/Abstractions/ISubmissionRepository.cs b/src/CodeClash.Domain/Abstractions/ISubmissionRepository.cs index b5204bf..5ac8fcf 100644 --- a/src/CodeClash.Domain/Abstractions/ISubmissionRepository.cs +++ b/src/CodeClash.Domain/Abstractions/ISubmissionRepository.cs @@ -6,13 +6,15 @@ public interface ISubmissionRepository { Task GetByIdAsync(Guid id); - IQueryable GetAllSubmissions(Guid problemId, string userId); + Task> GetAllSubmissions(Guid problemId, string userId); - IQueryable GetSolvedSubmissions(Guid problemId, string userId); + Task> GetSolvedSubmissions(Guid problemId, string userId); Task> GetUserAcceptedSubmissions(string userId); Task> GetSolvedProblemIdsAsync(List problemIds, string userId); Task> GetUserSubmissionsAsync(string userId); + + Task GetSubmissionIfAuthorized(string userId, Guid submissionId); } diff --git a/src/CodeClash.Domain/Premitives/Helper.cs b/src/CodeClash.Domain/Premitives/Helper.cs index 21f0af8..a01155f 100644 --- a/src/CodeClash.Domain/Premitives/Helper.cs +++ b/src/CodeClash.Domain/Premitives/Helper.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Text.Json; +using System.Text.RegularExpressions; namespace CodeClash.Domain.Premitives; public static class Helper @@ -8,6 +9,21 @@ public static class Helper public const string CppCompiler = "gcc:latest"; public const string CSharpCompiler = "mcr.microsoft.com/dotnet/sdk:5.0"; + public static T DeserializeObject(string json) + { + return JsonSerializer.Deserialize(json); + } + + public static IEnumerable DeserializeCollection(string json) + { + return JsonSerializer.Deserialize>(json); + } + + public static string Serialize(T obj) + { + return JsonSerializer.Serialize(obj); + } + public static string SetScriptFilePath() { string currentDirectory = Directory.GetCurrentDirectory(); diff --git a/src/CodeClash.Domain/Premitives/Responses/ProblemResponse.cs b/src/CodeClash.Domain/Premitives/Responses/ProblemResponse.cs new file mode 100644 index 0000000..5cf2edd --- /dev/null +++ b/src/CodeClash.Domain/Premitives/Responses/ProblemResponse.cs @@ -0,0 +1,4 @@ +namespace CodeClash.Domain.Premitives.Responses; +public record ProblemResponse( + Guid Id, + string Name); diff --git a/src/CodeClash.Infrastructure/DependencyInjection.cs b/src/CodeClash.Infrastructure/DependencyInjection.cs index c383f7a..7206166 100644 --- a/src/CodeClash.Infrastructure/DependencyInjection.cs +++ b/src/CodeClash.Infrastructure/DependencyInjection.cs @@ -49,6 +49,8 @@ public static IServiceCollection AddInfrastructure( services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); diff --git a/src/CodeClash.Infrastructure/Implementation/ResponseCacheService.cs b/src/CodeClash.Infrastructure/Implementation/ResponseCacheService.cs index e6b87f4..b510f16 100644 --- a/src/CodeClash.Infrastructure/Implementation/ResponseCacheService.cs +++ b/src/CodeClash.Infrastructure/Implementation/ResponseCacheService.cs @@ -45,8 +45,7 @@ public async Task CacheResponseAsync( /// /// Retrieves and deserializes a cached response from Redis. /// - public async Task> GetCachedResponseAsync( - string key) where T : class + public async Task GetCachedResponseAsync(string key) { // Retrieve value from Redis var value = await _database.StringGetAsync(key); @@ -57,16 +56,6 @@ public async Task> GetCachedResponseAsync( return null; } - // Cast RedisValue to string explicitly to avoid null warning - var json = (string?)value; - - // Ensure cached JSON is not empty - if (string.IsNullOrWhiteSpace(json)) - { - return null; - } - - // Deserialize JSON back into collection of T - return JsonSerializer.Deserialize>(json, _serializerOptions); + return value; } } diff --git a/src/CodeClash.Infrastructure/Repositories/BlogRepository.cs b/src/CodeClash.Infrastructure/Repositories/BlogRepository.cs new file mode 100644 index 0000000..082b01c --- /dev/null +++ b/src/CodeClash.Infrastructure/Repositories/BlogRepository.cs @@ -0,0 +1,33 @@ +using CodeClash.Domain.Abstractions; +using CodeClash.Domain.Models.Blogs; +using CodeClash.Domain.Models.Problems; +using Microsoft.EntityFrameworkCore; + +namespace CodeClash.Infrastructure.Repositories; +internal sealed class BlogRepository( + ApplicationDbContext context) : IBlogRepository +{ + public Task AddProblemToBlogAsync( + Guid blogId, + Guid problemId) + { + throw new NotImplementedException(); + } + + public Task AddSolutionToBlogAsync( + Guid blogId, + string solutionContent) + { + throw new NotImplementedException(); + } + + public Task> GetBlogsByProblemIdAsync(Guid problemId) + { + throw new NotImplementedException(); + } + + public async Task> GetProblemsForBlogAsync() + { + return await context.Problems.ToListAsync(); + } +} diff --git a/src/CodeClash.Infrastructure/Repositories/SubmissionRepository.cs b/src/CodeClash.Infrastructure/Repositories/SubmissionRepository.cs index af2fc86..cc164a4 100644 --- a/src/CodeClash.Infrastructure/Repositories/SubmissionRepository.cs +++ b/src/CodeClash.Infrastructure/Repositories/SubmissionRepository.cs @@ -17,11 +17,23 @@ public SubmissionRepository( public async Task GetByIdAsync(Guid id) => await _context.Submits.FirstOrDefaultAsync(x => x.Id == id); - public IQueryable GetAllSubmissions(Guid problemId, string userId) - => _context.Submits.Where(x => x.ProblemId == problemId && x.UserId == userId); + public async Task> GetAllSubmissions( + Guid problemId, + string userId) + { + return await _context.Submits + .Where(x => x.ProblemId == problemId && x.UserId == userId) + .ToListAsync(); + } - public IQueryable GetSolvedSubmissions(Guid problemId, string userId) - => _context.Submits.Where(x => x.ProblemId == problemId && x.UserId == userId && x.Result == SubmissionResult.Accepted); + public async Task> GetSolvedSubmissions( + Guid problemId, + string userId) + { + return await _context.Submits + .Where(x => x.ProblemId == problemId && x.UserId == userId && x.Result == SubmissionResult.Accepted) + .ToListAsync(); + } public async Task> GetUserAcceptedSubmissions( string userId) @@ -63,4 +75,27 @@ public async Task> GetUserSubmissionsAsync( }) .ToDictionaryAsync(x => x.ProblemId, x => x.Result); } + + public async Task GetSubmissionIfAuthorized( + string userId, + Guid submissionId) + { + var submission = await _context.Submits + .Include(x => x.Contest) + .FirstOrDefaultAsync(x => x.Id == submissionId); + + if (submission is null) + { + return null; + } + + if (submission.Contest is not null + && submission.Contest.ContestStatus == ContestStatus.Running + && submission.UserId != userId) + { + return null; + } + + return submission; + } }