diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index 9a1a036..eca883b 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -4,10 +4,12 @@ on: pull_request: branches: [main] workflow_dispatch: +env: + DOTNET_INSTALL_DIR: "./.dotnet" jobs: build: - runs-on: shimmer-jaspilite + runs-on: self-hosted steps: - name: Checkout code uses: actions/checkout@v2 @@ -19,14 +21,14 @@ jobs: # - name: Generate appsettings.json # run: | - # (Get-Content ./AuthMetodology/appsettings.Template.json) -replace '#{POSTGRES_CONNECTION_STRING}#', '${{ secrets.POSTGRES_CONNECTION_STRING }}' -replace '#{GOOGLE_CLIENT_ID}#', '${{ secrets.GOOGLE_CLIENT_ID }}' -replace '#{JWT_SECRET_KEY}#', '${{ secrets.JWT_SECRET_KEY }}' | Set-Content ./AuthMetodology/appsettings.json + # (Get-Content ./MeethodologyMain.API/appsettings.Template.json) -replace '#{POSTGRES_CONNECTION_STRING}#', '${{ secrets.POSTGRES_CONNECTION_STRING }}' -replace '#{GOOGLE_CLIENT_ID}#', '${{ secrets.GOOGLE_CLIENT_ID }}' -replace '#{JWT_SECRET_KEY}#', '${{ secrets.JWT_SECRET_KEY }}' | Set-Content ./AuthMetodology/appsettings.json # shell: pwsh - name: Restore dependencies - run: dotnet restore --verbosity quiet + run: dotnet restore --verbosity quiet ./MethodologyMain.sln - name: Build without warnings - run: dotnet build --configuration Release --no-restore + run: dotnet build --configuration Release --no-restore ./MethodologyMain.sln # - name: Run tests # run: dotnet test --verbosity normal diff --git a/MethodologyMain.API/Controllers/TeamController.cs b/MethodologyMain.API/Controllers/TeamController.cs index 6dc5ad0..9d780da 100644 --- a/MethodologyMain.API/Controllers/TeamController.cs +++ b/MethodologyMain.API/Controllers/TeamController.cs @@ -1,198 +1,319 @@ using MethodologyMain.Application.DTO; using MethodTeams.Models; -using Microsoft.AspNetCore.Authorization; +//using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using System.Security.Claims; +//using System.Security.Claims; using MethodologyMain.Application.Interface; - +using AuthMetodology.Infrastructure.Models; +using Serilog.Events; +using RabbitMqPublisher.Interface; namespace MethodologyMain.API.Controllers { [ApiController] [Route("api/[controller]")] public class TeamController : ControllerBase { - private readonly ITeamService _teamService; + private readonly ITeamService teamService; + private readonly IRabbitMqPublisherBase logPublishService; + public TeamController(ITeamService teamService, IRabbitMqPublisherBase logPublishService) + { + this.teamService = teamService; + this.logPublishService = logPublishService; + } - public TeamController(ITeamService teamService) + //Тестовый эндпоинт + [HttpGet("dummy-data")] + public async Task GetDummyData(CancellationToken cancellationToken) { - _teamService = teamService; + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish + { + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "POST api/Team/dummy-data was called", + TimeStamp = DateTime.UtcNow + }, cancellationToken); + string data = "Dataaa"; + return Ok(data); } // Создание команды [HttpPost] //[Authorize] - public async Task> CreateTeam([FromBody] CreateTeamDto dto) + public async Task> CreateTeam([FromBody] CreateTeamDto dto, CancellationToken token) { - try - { - Guid currentUserId = GetCurrentUserId(); // Получение ID текущего пользователя из токена - var team = await _teamService.CreateTeamAsync(dto.Name, dto.Description, currentUserId, dto.EventId); - return CreatedAtAction(nameof(GetTeam), new { id = team.Id }, team); - } - catch (InvalidOperationException ex) + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - return BadRequest(ex.Message); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "POST api/Team was called", + TimeStamp = DateTime.UtcNow + }, token); + Guid currentUserId = GetCurrentUserId(); // Получение ID текущего пользователя из токена + var team = await teamService.CreateTeamAsync(dto.Name, dto.Description, currentUserId, dto.EventId, token); + return CreatedAtAction(nameof(GetTeam), new { id = team.Id }, team); + //try + //{ + + //} + //catch (InvalidOperationException ex) + //{ + // return BadRequest(ex.Message); + //} } // Получение информации о команде [HttpGet("{id}")] - public async Task> GetTeam(Guid id) + public async Task> GetTeam(Guid id, CancellationToken token) { - try + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - var team = await _teamService.GetTeamByIdAsync(id); - return Ok(team); - } - catch (KeyNotFoundException) - { - return NotFound(); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "GET api/Team/id was called", + TimeStamp = DateTime.UtcNow + }, token); + var team = await teamService.GetTeamByIdAsync(id, token); + return Ok(team); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} } // Удаление команды [HttpDelete("{id}")] //[Authorize] - public async Task DeleteTeam(Guid id) + public async Task DeleteTeam(Guid id, CancellationToken token) { - try + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - Guid currentUserId = GetCurrentUserId(); - bool isAdmin = User.IsInRole("Admin"); // Проверка роли администратора + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "DELETE api/Team/id was called", + TimeStamp = DateTime.UtcNow + }, token); + Guid currentUserId = GetCurrentUserId(); + bool isAdmin = User.IsInRole("Admin"); // Проверка роли администратора - await _teamService.DeleteTeamAsync(id, currentUserId, isAdmin); - return NoContent(); - } - catch (KeyNotFoundException) - { - return NotFound(); - } - catch (UnauthorizedAccessException ex) - { - return Forbid(ex.Message); - } + await teamService.DeleteTeamAsync(id, currentUserId, token, isAdmin); + return NoContent(); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} + //catch (UnauthorizedAccessException ex) + //{ + // return Forbid(ex.Message); + //} } // Добавление пользователя в команду - [HttpPost("{id}/members")] + [HttpPost("{id}/users")] //[Authorize] - public async Task AddMember(Guid id, [FromBody] AddUserDto dto) + public async Task AddMember(Guid id, [FromBody] AddUserDto dto, CancellationToken token) { - try - { - Guid currentUserId = GetCurrentUserId(); - await _teamService.AddUserToTeamAsync(id, dto.UserId, currentUserId); - return NoContent(); - } - catch (KeyNotFoundException) + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - return NotFound(); - } - catch (UnauthorizedAccessException ex) - { - return Forbid(ex.Message); - } - catch (InvalidOperationException ex) - { - return BadRequest(ex.Message); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "POST api/Team/id/members was called", + TimeStamp = DateTime.UtcNow + }, token); + Guid currentUserId = GetCurrentUserId(); + await teamService.AddUserToTeamAsync(id, dto.UserId, currentUserId, token); + return NoContent(); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} + //catch (UnauthorizedAccessException ex) + //{ + // return Forbid(ex.Message); + //} + //catch (InvalidOperationException ex) + //{ + // return BadRequest(ex.Message); + //} } // Удаление пользователя из команды - [HttpDelete("{id}/members/{userId}")] + [HttpDelete("{id}/users/{userId}")] //[Authorize] - public async Task RemoveMember(Guid id, Guid userId) + public async Task RemoveMember(Guid id, Guid userId, CancellationToken token) { - try - { - Guid currentUserId = GetCurrentUserId(); - await _teamService.RemoveUserFromTeamAsync(id, userId, currentUserId); - return NoContent(); - } - catch (KeyNotFoundException) - { - return NotFound(); - } - catch (UnauthorizedAccessException ex) + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - return Forbid(ex.Message); - } - catch (InvalidOperationException ex) - { - return BadRequest(ex.Message); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "DELETE api/Team/id/members/userId was called", + TimeStamp = DateTime.UtcNow + }, token); + Guid currentUserId = GetCurrentUserId(); + await teamService.RemoveUserFromTeamAsync(id, userId, currentUserId, token); + return NoContent(); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} + //catch (UnauthorizedAccessException ex) + //{ + // return Forbid(ex.Message); + //} + //catch (InvalidOperationException ex) + //{ + // return BadRequest(ex.Message); + //} } // Получение списка участников команды - [HttpGet("{id}/members")] - public async Task>> GetTeamMembers(Guid id) + [HttpGet("{id}/users")] + public async Task>> GetTeamMembers(Guid id, CancellationToken token) { - try - { - var members = await _teamService.GetTeamMembersAsync(id); - return Ok(members); - } - catch (KeyNotFoundException) + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - return NotFound(); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "GET api/Team/id/members was called", + TimeStamp = DateTime.UtcNow + }, token); + var members = await teamService.GetTeamMembersAsync(id, token); + return Ok(members); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} } // Передача прав капитана [HttpPut("{id}/captain")] //[Authorize] - public async Task TransferCaptainRights(Guid id, [FromBody] AddUserDto dto) + public async Task TransferCaptainRights(Guid id, [FromBody] AddUserDto dto, CancellationToken token) { - try - { - Guid currentUserId = GetCurrentUserId(); - await _teamService.TransferCaptainRightsAsync(id, dto.UserId, currentUserId); - return NoContent(); - } - catch (KeyNotFoundException) - { - return NotFound(); - } - catch (UnauthorizedAccessException ex) - { - return Forbid(ex.Message); - } - catch (InvalidOperationException ex) + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish { - return BadRequest(ex.Message); - } + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "PUT api/Team/id/captain was called", + TimeStamp = DateTime.UtcNow + }, token); + Guid currentUserId = GetCurrentUserId(); + await teamService.TransferCaptainRightsAsync(id, dto.UserId, currentUserId, token); + return NoContent(); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} + //catch (UnauthorizedAccessException ex) + //{ + // return Forbid(ex.Message); + //} + //catch (InvalidOperationException ex) + //{ + // return BadRequest(ex.Message); + //} } - // Получение списка команд для события - [HttpGet("event/{eventId}")] - public async Task>> GetTeamsByEvent(Guid eventId) + // Получение списка команд + [HttpGet] + public async Task>> GetTeamsAll(CancellationToken token) { - var teams = await _teamService.GetTeamsByEventIdAsync(eventId); + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish + { + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "GET api/Team was called", + TimeStamp = DateTime.UtcNow + }, token); + var teams = await teamService.GetTeamAllAsync(token); return Ok(teams); + //try + //{ + + //} + //catch (KeyNotFoundException) + //{ + // return NotFound(); + //} + //catch (UnauthorizedAccessException ex) + //{ + // return Forbid(ex.Message); + //} + //catch (InvalidOperationException ex) + //{ + // return BadRequest(ex.Message); + //} } - // Получение команды пользователя для конкретного события - [HttpGet("event/{eventId}/user")] - //[Authorize] - public async Task> GetUserTeamForEvent(Guid eventId) - { - try - { - Guid currentUserId = GetCurrentUserId(); - var team = await _teamService.GetUserTeamForEventAsync(currentUserId, eventId); + //// Получение списка команд для события + //[HttpGet("event/{eventId}")] + //public async Task>> GetTeamsByEvent(Guid eventId) + //{ + // await logQueueService.SendLogEventAsync(new RabbitMqLogPublish + // { + // ServiceName = "Main service", + // LogLevel = LogEventLevel.Information, + // Message = "GET api/Team/event/eventId was called", + // TimeStamp = DateTime.UtcNow + // }); + // var teams = await _teamService.GetTeamsByEventIdAsync(eventId); + // return Ok(teams); + //} - if (team == null) - { - return NotFound(); - } + //// Получение команды пользователя для конкретного события + //[HttpGet("event/{eventId}/user")] + ////[Authorize] + //public async Task> GetUserTeamForEvent(Guid eventId) + //{ + // await logQueueService.SendLogEventAsync(new RabbitMqLogPublish + // { + // ServiceName = "Main service", + // LogLevel = LogEventLevel.Information, + // Message = "GET api/Team/event/eventId/user was called", + // TimeStamp = DateTime.UtcNow + // }); + // Guid currentUserId = GetCurrentUserId(); + // var team = await _teamService.GetUserTeamForEventAsync(currentUserId, eventId); - return Ok(team); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } + // if (team == null) + // { + // return NotFound(); + // } + + // return Ok(team); + + //try + //{ + + //} + //catch (Exception ex) + //{ + // return BadRequest(ex.Message); + //} + //} // Вспомогательный метод для получения ID текущего пользователя //private int GetCurrentUserId() diff --git a/MethodologyMain.API/Dockerfile b/MethodologyMain.API/Dockerfile index 12a01a3..c641a52 100644 --- a/MethodologyMain.API/Dockerfile +++ b/MethodologyMain.API/Dockerfile @@ -1,30 +1,28 @@ -# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки. +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки) -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER $APP_UID -WORKDIR /app -EXPOSE 8080 -EXPOSE 8081 +WORKDIR /src + +COPY ["MethodologyMain.API/MethodologyMain.API.csproj", "MethodologyMain.API/"] +COPY ["MethodologyMain.Application/MethodologyMain.Application.csproj","MethodologyMain.Application/"] +COPY ["MethodologyMain.Infrastructure/MethodologyMain.Infrastructure.csproj","MethodologyMain.Infrastructure/"] +COPY ["MethodologyMain.Logic/MethodologyMain.Logic.csproj","MethodologyMain.Logic/"] +COPY ["MethodologyMain.Persistence/MethodologyMain.Persistence.csproj","MethodologyMain.Persistence/"] + +RUN dotnet restore "MethodologyMain.API/MethodologyMain.API.csproj" -# Этот этап используется для сборки проекта службы -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["MethodTeams1/MethodTeams1.csproj", "MethodTeams1/"] -RUN dotnet restore "./MethodTeams1/MethodTeams1.csproj" COPY . . -WORKDIR "/src/MethodTeams1" -RUN dotnet build "./MethodTeams1.csproj" -c $BUILD_CONFIGURATION -o /app/build -# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./MethodTeams1.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false +WORKDIR "/src/MethodologyMain.API" +RUN dotnet publish -c Release -o /app/publish + + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime -# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется) -FROM base AS final WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "MethodTeams1.dll"] \ No newline at end of file + +EXPOSE 80 + +COPY --from=build /app/publish . + +ENTRYPOINT ["dotnet", "MethodologyMain.API.dll"] \ No newline at end of file diff --git a/MethodologyMain.API/Extensions/AuthExtensions.cs b/MethodologyMain.API/Extensions/AuthExtensions.cs new file mode 100644 index 0000000..c65b833 --- /dev/null +++ b/MethodologyMain.API/Extensions/AuthExtensions.cs @@ -0,0 +1,44 @@ +using MethodologyMain.Infrastructure.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace MethodologyMain.API.Extensions +{ + public static class AuthExtensions + { + public static void AddApiAuthentication( + this IServiceCollection services, + IOptions options) + { + var value = options.Value; + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { //будет осуществляться проверка, есть ли в headers токен + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(value.SecurityKey)) + }; + + options.Events = new JwtBearerEvents() + { + OnMessageReceived = context => + { + context.Token = context.Request.Cookies["access"]; + + return Task.CompletedTask; + } + }; + }); + services.AddAuthorizationBuilder() + //.AddPolicy("AdminOnly", policy => policy.RequireRole(nameof(UserRole.Admin))) + .AddPolicy("BearerOnly", policy => { policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); }); + } + } +} diff --git a/MethodologyMain.API/MethodologyMain.API.csproj b/MethodologyMain.API/MethodologyMain.API.csproj index af8b76e..f777033 100644 --- a/MethodologyMain.API/MethodologyMain.API.csproj +++ b/MethodologyMain.API/MethodologyMain.API.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -6,17 +6,20 @@ enable 1fe97921-83f1-4b1c-8a25-61646ab7998e Linux + ..\docker-compose.dcproj - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -27,4 +30,16 @@ + + + ..\MethodologyMain.Infrastructure\DLL\RabbitMqListener.dll + + + ..\MethodologyMain.Infrastructure\DLL\RabbitMqModel.dll + + + ..\MethodologyMain.Infrastructure\DLL\RabbitMqPublisher.dll + + + diff --git a/MethodologyMain.API/Middleware/ExceptionMiddleware.cs b/MethodologyMain.API/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000..b758ea2 --- /dev/null +++ b/MethodologyMain.API/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,56 @@ +using AuthMetodology.Infrastructure.Models; +using MethodologyMain.Application.Exceptions; +using RabbitMqPublisher.Interface; +using Serilog.Events; + +namespace MethodologyMain.API.Middleware +{ + public class ExceptionMiddleware + { + private readonly RequestDelegate next; + private readonly IRabbitMqPublisherBase logService; + public ExceptionMiddleware(RequestDelegate next, IRabbitMqPublisherBase logService) + { + this.next = next; + this.logService = logService; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + _ = logService.SendEventAsync(new RabbitMqLogPublish + { + ServiceName = "Main service", + LogLevel = LogEventLevel.Error, + Message = $"Exception was thrown.\nMessage: {ex.Message}\nSource: {ex.Source}", + TimeStamp = DateTime.UtcNow + }); + + await HandleException(ex,context); + } + } + + private static async Task HandleException(Exception ex, HttpContext context) + { + ExceptionResponse response = ex switch + { + MemberAlreadyInTeamException _ => new ExceptionResponse("Пользователь уже состоит в команде для данного события", System.Net.HttpStatusCode.Conflict), + TeamNotFoundException _ => new ExceptionResponse("Команда не найдена", System.Net.HttpStatusCode.Conflict), + UnauthorizedAccessException _ => new ExceptionResponse("У вас не таких прав", System.Net.HttpStatusCode.Unauthorized), + InvalidOperationException _ => new ExceptionResponse(ex.Message, System.Net.HttpStatusCode.Conflict), + UserNotFoundException _ => new ExceptionResponse("Пользователь не найден", System.Net.HttpStatusCode.Conflict), + UserNotInTeamException _ => new ExceptionResponse("Пользователь не в команде", System.Net.HttpStatusCode.Conflict), + _ => new ExceptionResponse(ex.Message, System.Net.HttpStatusCode.InternalServerError), + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)response.Code; + await context.Response.WriteAsJsonAsync(response); + } + } +} diff --git a/MethodologyMain.API/Middleware/ExceptionResponse.cs b/MethodologyMain.API/Middleware/ExceptionResponse.cs new file mode 100644 index 0000000..403ff12 --- /dev/null +++ b/MethodologyMain.API/Middleware/ExceptionResponse.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace MethodologyMain.API.Middleware +{ + public record ExceptionResponse(string Message, HttpStatusCode Code); +} diff --git a/MethodologyMain.API/Program.cs b/MethodologyMain.API/Program.cs index a401bba..584abd4 100644 --- a/MethodologyMain.API/Program.cs +++ b/MethodologyMain.API/Program.cs @@ -1,11 +1,28 @@ using MethodTeams.Data; using MethodTeams.Services; +using AutoMapper; using System.Text.Json.Serialization; using MethodologyMain.Application.Interface; +using MethodologyMain.Application.Services; +using MethodologyMain.API.Middleware; +using MethodologyMain.Infrastructure.Services; +using MethodologyMain.Persistence.Interfaces; +using MethodologyMain.Persistence.Repository; +using MethodTeams.DTO; +using System.Reflection; +using MethodologyMain.Application.Profiles; +using Microsoft.EntityFrameworkCore; +using RabbitMqModel.Models; +using RabbitMqPublisher.Interface; +using AuthMetodology.Infrastructure.Models; +using MethodologyMain.Infrastructure.Models; +using MethodologyMain.API.Extensions; +using Microsoft.Extensions.Options; +using RabbitMqListener.Interfaces; +using MethodologyMain.Infrastructure.Listeners; var builder = WebApplication.CreateBuilder(args); - builder.Services.AddControllers(); builder.Services.AddControllers() .AddJsonOptions(options => @@ -14,8 +31,34 @@ }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddScoped(); -builder.Services.AddDbContext(); + +var configuration = new MapperConfiguration(static cfg => +{ + cfg.AddMaps(Assembly.GetExecutingAssembly()); + cfg.AllowNullCollections = true; + cfg.AddGlobalIgnore("Item"); +} +); +IMapper mapper = configuration.CreateMapper(); +builder.Services.AddSingleton(mapper); + +builder.Services.Configure(builder.Configuration.GetSection(nameof(RabbitMqOptions))); +builder.Services.Configure(builder.Configuration.GetSection(nameof(JWTOptions))); + +builder.Services.AddApiAuthentication(builder.Services.BuildServiceProvider().GetRequiredService>()); + +builder.Services.AddScoped(); +builder.Services.AddAutoMapper(typeof(TeamProfile).Assembly, typeof(TeamInfoDto).Assembly); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton, LogQueueService>(); + +builder.Services.AddHostedService() + .AddSingleton(); + +var connection = builder.Configuration.GetConnectionString("PostgresConnection"); +builder.Services.AddDbContext(opt => opt.UseNpgsql(connection)); var app = builder.Build(); @@ -28,7 +71,10 @@ app.UseHttpsRedirection(); app.UseAuthorization(); +app.UseAuthentication(); app.MapControllers(); +app.UseMiddleware(); + app.Run(); diff --git a/MethodologyMain.API/Properties/launchSettings.json b/MethodologyMain.API/Properties/launchSettings.json index 5705205..3f97ff6 100644 --- a/MethodologyMain.API/Properties/launchSettings.json +++ b/MethodologyMain.API/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "http": { "commandName": "Project", - "launchBrowser": true, + //"launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/MethodologyMain.API/Properties/serviceDependencies.json b/MethodologyMain.API/Properties/serviceDependencies.json index 33703d5..5cee86d 100644 --- a/MethodologyMain.API/Properties/serviceDependencies.json +++ b/MethodologyMain.API/Properties/serviceDependencies.json @@ -1,3 +1,12 @@ { - "dependencies": {} + "dependencies": { + "rabbitmq1": { + "type": "rabbitmq", + "connectionId": "QueueConnection" + }, + "postgresql1": { + "type": "postgresql", + "connectionId": "ConnectionStrings:DatabaseConnection" + } + } } \ No newline at end of file diff --git a/MethodologyMain.API/appsettings.json b/MethodologyMain.API/appsettings.json index 2567da7..fdde830 100644 --- a/MethodologyMain.API/appsettings.json +++ b/MethodologyMain.API/appsettings.json @@ -7,7 +7,44 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "PostgresConnection": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=Password" + "PostgresConnection": "Host=main_db;Port=5432;Database=maindb;Username=admin;Password=password" + }, + "RabbitMqOptions": { + "Host": "rabbitmq", + "Port": "5672" + }, + "JWTOptions": { + "SecurityKey": "secret" + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Grafana.Loki" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.Hosting.Lifetime": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "WriteTo": [ + { + "Name": "GrafanaLoki", + "Args": { + "uri": "http://localhost:3100", + "labels": [ + { + "key": "AuthApp", + "value": "My Main App" + } + ], + "propertiesAsLabels": [ + "AuthApp" + ] + } + } + ] } + } diff --git a/MethodologyMain.Application/DTO/TeamInfoDto.cs b/MethodologyMain.Application/DTO/TeamInfoDto.cs index 2533612..182b720 100644 --- a/MethodologyMain.Application/DTO/TeamInfoDto.cs +++ b/MethodologyMain.Application/DTO/TeamInfoDto.cs @@ -6,9 +6,9 @@ public class TeamInfoDto { public Guid Id { get; set; } public string Name { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public Guid CaptainId { get; set; } - public Guid EventId { get; set; } + public Guid HackathonId { get; set; } public DateTime CreatedAt { get; set; } public int MemberCount { get; set; } } diff --git a/MethodologyMain.Application/Exceptions/MemberAlreadyInTeamException.cs b/MethodologyMain.Application/Exceptions/MemberAlreadyInTeamException.cs new file mode 100644 index 0000000..4588b3f --- /dev/null +++ b/MethodologyMain.Application/Exceptions/MemberAlreadyInTeamException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MethodologyMain.Application.Exceptions +{ + public class MemberAlreadyInTeamException : Exception + { + public MemberAlreadyInTeamException() + { + + } + + public MemberAlreadyInTeamException(string message) + : base(message) + { + + } + } +} diff --git a/MethodologyMain.Application/Exceptions/TeamNotFoundException.cs b/MethodologyMain.Application/Exceptions/TeamNotFoundException.cs new file mode 100644 index 0000000..f898077 --- /dev/null +++ b/MethodologyMain.Application/Exceptions/TeamNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MethodologyMain.Application.Exceptions +{ + public class TeamNotFoundException : Exception + { + public TeamNotFoundException() { } + + public TeamNotFoundException(string message) + :base(message) + { + + } + } +} diff --git a/MethodologyMain.Application/Exceptions/UserNotFoundException.cs b/MethodologyMain.Application/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..07357b5 --- /dev/null +++ b/MethodologyMain.Application/Exceptions/UserNotFoundException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MethodologyMain.Application.Exceptions +{ + public class UserNotFoundException : Exception + { + public UserNotFoundException() { } + + public UserNotFoundException(string message) + :base(message) + { + + } + } +} diff --git a/MethodologyMain.Application/Exceptions/UserNotInTeamException.cs b/MethodologyMain.Application/Exceptions/UserNotInTeamException.cs new file mode 100644 index 0000000..e3690ef --- /dev/null +++ b/MethodologyMain.Application/Exceptions/UserNotInTeamException.cs @@ -0,0 +1,16 @@ +namespace MethodologyMain.Application.Exceptions +{ + public class UserNotInTeamException : Exception + { + public UserNotInTeamException() + { + + } + + public UserNotInTeamException(string message) + : base(message) + { + + } + } +} \ No newline at end of file diff --git a/MethodologyMain.Application/Interface/ITeamService.cs b/MethodologyMain.Application/Interface/ITeamService.cs index 1458842..b5480b3 100644 --- a/MethodologyMain.Application/Interface/ITeamService.cs +++ b/MethodologyMain.Application/Interface/ITeamService.cs @@ -1,18 +1,20 @@ using MethodologyMain.Logic.Entities; +using MethodTeams.DTO; namespace MethodologyMain.Application.Interface { public interface ITeamService { - Task AddUserToTeamAsync(Guid teamId, Guid userId, Guid requestingUserId); - Task CreateTeamAsync(string name, string description, Guid captainId, Guid eventId); - Task DeleteTeamAsync(Guid teamId, Guid requestingUserId, bool isAdmin = false); - Task GetTeamByIdAsync(Guid teamId); - Task> GetTeamMembersAsync(Guid teamId); + Task AddUserToTeamAsync(Guid teamId, Guid userId, Guid requestingUserId, CancellationToken token); + Task CreateTeamAsync(string name, string description, Guid captainId, Guid eventId, CancellationToken token); + Task DeleteTeamAsync(Guid teamId, Guid requestingUserId, CancellationToken token, bool isAdmin = false); + Task GetTeamByIdAsync(Guid teamId, CancellationToken token); + Task> GetTeamMembersAsync(Guid teamId, CancellationToken token); + Task> GetTeamAllAsync(CancellationToken token); //Task> GetTeamsByEventIdAsync(Guid eventId); //Task GetUserTeamForEventAsync(Guid userId, Guid eventId); //Task IsUserTeamCaptainAsync(Guid teamId, Guid userId); - Task RemoveUserFromTeamAsync(Guid teamId, Guid userId, Guid requestingUserId); - Task TransferCaptainRightsAsync(Guid teamId, Guid newCaptainId, Guid currentCaptainId); + Task RemoveUserFromTeamAsync(Guid teamId, Guid userId, Guid requestingUserId, CancellationToken token); + Task TransferCaptainRightsAsync(Guid teamId, Guid newCaptainId, Guid currentCaptainId, CancellationToken token); } } \ No newline at end of file diff --git a/MethodologyMain.Application/Interface/ITeamValidationService.cs b/MethodologyMain.Application/Interface/ITeamValidationService.cs new file mode 100644 index 0000000..2303803 --- /dev/null +++ b/MethodologyMain.Application/Interface/ITeamValidationService.cs @@ -0,0 +1,14 @@ +namespace MethodologyMain.Application.Interface +{ + public interface ITeamValidationService + { + Task CheckCaptainKick(Guid teamId, Guid userId, CancellationToken token); + Task CheckTeamExistsAsync(Guid teamId, CancellationToken token); + Task CheckUserInTeamAsync(Guid userId, Guid teamId, CancellationToken token); + Task CheckUserIsCaptainAsync(Guid teamId, Guid userId, CancellationToken token); + Task CheckUserIsCaptainOrAdminAsync(Guid teamId, Guid userId, bool isAdmin, CancellationToken token); + Task CheckUserIsCaptainOrRequestedAsync(Guid teamId, Guid userId, Guid requestingUserId, CancellationToken token); + Task CheckUserNotInAnyTeamForHackathonAsync(Guid userId, Guid hackathonId, CancellationToken token); + Task CheckUserNotInTeamAsync(Guid userId, Guid teamId, CancellationToken token); + } +} \ No newline at end of file diff --git a/MethodologyMain.Application/MethodologyMain.Application.csproj b/MethodologyMain.Application/MethodologyMain.Application.csproj index cd6da6a..4669d23 100644 --- a/MethodologyMain.Application/MethodologyMain.Application.csproj +++ b/MethodologyMain.Application/MethodologyMain.Application.csproj @@ -1,7 +1,7 @@  - Exe + Library net8.0 enable enable diff --git a/MethodologyMain.Application/Services/TeamService.cs b/MethodologyMain.Application/Services/TeamService.cs index 52c8466..dfeb4a7 100644 --- a/MethodologyMain.Application/Services/TeamService.cs +++ b/MethodologyMain.Application/Services/TeamService.cs @@ -1,27 +1,40 @@ -using Microsoft.EntityFrameworkCore; -using AutoMapper; -using MethodologyMain.Persistence.Interfaces; +using MethodologyMain.Persistence.Interfaces; using MethodologyMain.Application.Interface; using MethodologyMain.Logic.Entities; +using MethodologyMain.Application.Exceptions; +using MethodTeams.Models; +using AutoMapper; +using MethodologyMain.Logic.Models; +using MethodTeams.DTO; namespace MethodTeams.Services { - public class TeamService : ITeamService + public class TeamService: ITeamService { private readonly ITeamRepository teamRepo; - public TeamService(ITeamRepository teamRepo) + private readonly ITeamValidationService validation; + private readonly IMapper mapper; + public TeamService( + ITeamRepository teamRepo, + ITeamValidationService validation, + IMapper mapper) { this.teamRepo = teamRepo; + this.validation = validation; + this.mapper = mapper; } // Создание новой команды - public async Task CreateTeamAsync(string name, string description, Guid captainId, Guid HackathonId) + public async Task CreateTeamAsync( + string name, + string description, + Guid captainId, + Guid HackathonId, + CancellationToken token + ) { // Проверка, что у пользователя нет другой команды для этого - if (await teamRepo.CheckUserTeamInHackAsync(captainId, HackathonId)) - { - throw new InvalidOperationException("Пользователь уже состоит в команде для данного события"); - } + await validation.CheckUserNotInAnyTeamForHackathonAsync(captainId, HackathonId, token); // Создание команды var team = new TeamEntity { @@ -40,131 +53,87 @@ public async Task CreateTeamAsync(string name, string description, G JoinedAt = DateTime.UtcNow }; team.Members.Add(member); - await teamRepo.AddAsync(team); + await teamRepo.AddAsync(team, token); return team; } // Удаление команды (только капитаном или администратором) - public async Task DeleteTeamAsync(Guid teamId, Guid requestingUserId, bool isAdmin = false) + public async Task DeleteTeamAsync( + Guid teamId, + Guid requestingUserId, + CancellationToken token, + bool isAdmin = false + ) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } - - if (await teamRepo.GetCaptainIdAsync(teamId) != requestingUserId && !isAdmin) - { - throw new UnauthorizedAccessException("Только капитан команды или администратор может удалить команду"); - } - - await teamRepo.RemoveTeamAsync(teamId); + await validation.CheckTeamExistsAsync(teamId, token); + await validation.CheckUserIsCaptainOrAdminAsync(teamId, requestingUserId, isAdmin, token); + await teamRepo.RemoveTeamAsync(teamId, token); } // Добавление пользователя в команду - public async Task AddUserToTeamAsync(Guid teamId, Guid userId, Guid requestingUserId) + public async Task AddUserToTeamAsync( + Guid teamId, + Guid userId, + Guid requestingUserId, + CancellationToken token + ) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } - - Guid hackathonId = await teamRepo.GetHackathonIdAsync(teamId); - - if (await teamRepo.GetCaptainIdAsync(teamId) != requestingUserId) - { - throw new UnauthorizedAccessException("Только капитан команды может добавлять участников"); - } - - // Проверка, что пользователь не состоит в команде этого события - if (await teamRepo.CheckUserTeamInHackAsync(userId, hackathonId)) - { - throw new InvalidOperationException("Пользователь уже состоит в команде для данного события"); - } - - // Проверка, что пользователь не является уже членом этой команды - if (await teamRepo.CheckUserInTeamAsync(userId, teamId)) - { - throw new InvalidOperationException("Пользователь уже состоит в этой команде"); - } - - // Добавление пользователя в команду - await teamRepo.AddMemberAsync(userId, teamId); + await validation.CheckTeamExistsAsync(teamId, token); + Guid hackathonId = (Guid)await teamRepo.GetHackathonIdAsync(teamId, token); + await validation.CheckUserIsCaptainAsync(teamId, requestingUserId, token); + await validation.CheckUserNotInTeamAsync(userId, teamId, token); + await validation.CheckUserNotInAnyTeamForHackathonAsync(userId, hackathonId, token); + await teamRepo.AddMemberAsync(userId, teamId, token); } // Удаление пользователя из команды - public async Task RemoveUserFromTeamAsync(Guid teamId, Guid userId, Guid requestingUserId) + public async Task RemoveUserFromTeamAsync( + Guid teamId, + Guid userId, + Guid requestingUserId, + CancellationToken token + ) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } - + await validation.CheckTeamExistsAsync(teamId, token); // Проверка прав: капитан может удалить любого, участник - только себя - Guid captainId = await teamRepo.GetCaptainIdAsync(teamId); - bool canRemove = (captainId == requestingUserId) || (userId == requestingUserId); - - if (!canRemove) - { - throw new UnauthorizedAccessException("Недостаточно прав для удаления участника"); - } - + await validation.CheckUserIsCaptainOrRequestedAsync(teamId, userId, requestingUserId, token); // Нельзя удалить капитана - if (userId == captainId) - { - throw new InvalidOperationException("Капитан не может быть удален из команды. Сначала передайте права капитана другому участнику."); - } - - if (!await teamRepo.CheckUserInTeamAsync(userId, teamId)) - { - throw new KeyNotFoundException("Пользователь не найден в команде"); - } - - await teamRepo.RemoveMemberAsync(userId, teamId); + await validation.CheckCaptainKick(teamId, userId, token); + await validation.CheckUserInTeamAsync(userId, teamId, token); + await teamRepo.RemoveMemberAsync(userId, teamId, token); } - // Передача прав капитана - public async Task TransferCaptainRightsAsync(Guid teamId, Guid newCaptainId, Guid currentCaptainId) + public async Task TransferCaptainRightsAsync( + Guid teamId, + Guid newCaptainId, + Guid currentCaptainId, + CancellationToken token + ) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } - - if (await teamRepo.GetCaptainIdAsync(teamId) != currentCaptainId) - { - throw new UnauthorizedAccessException("Только текущий капитан может передать права капитана"); - } - - // Проверка, что новый капитан является участником команды - if (!await teamRepo.CheckUserInTeamAsync(newCaptainId, teamId)) - { - throw new InvalidOperationException("Новый капитан должен быть участником команды"); - } - - // Передача прав - await teamRepo.TransferCaptainAsync(newCaptainId, teamId); + await validation.CheckTeamExistsAsync(teamId, token); + await validation.CheckUserIsCaptainAsync(teamId, currentCaptainId, token); + await validation.CheckUserInTeamAsync(newCaptainId, teamId, token); + await teamRepo.TransferCaptainAsync(newCaptainId, teamId, token); } - // Получение информации о команде по ID - public async Task GetTeamByIdAsync(Guid teamId) + public async Task GetTeamByIdAsync(Guid teamId, CancellationToken token) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } - - return await teamRepo.GetByIdAsync(teamId); + await validation.CheckTeamExistsAsync(teamId, token); + return await teamRepo.GetByIdAsync(teamId, token); } - // Получение списка участников команды - public async Task> GetTeamMembersAsync(Guid teamId) + public async Task> GetTeamMembersAsync(Guid teamId, CancellationToken token) { - if (!await teamRepo.CheckTeamExistAsync(teamId)) - { - throw new KeyNotFoundException("Команда не найдена"); - } + await validation.CheckTeamExistsAsync(teamId, token); + return await teamRepo.GetTeamMembersAsync(teamId, token); + } - return await teamRepo.GetTeamMembersAsync(teamId); + // Получение списка команд + public async Task> GetTeamAllAsync(CancellationToken token) + { + + var teams = await teamRepo.GetAllAsync(token); + return mapper.Map>(teams); } //// Проверка, является ли пользователь капитаном команды diff --git a/MethodologyMain.Application/Services/TeamValidationService.cs b/MethodologyMain.Application/Services/TeamValidationService.cs new file mode 100644 index 0000000..b92d86d --- /dev/null +++ b/MethodologyMain.Application/Services/TeamValidationService.cs @@ -0,0 +1,82 @@ +using MethodologyMain.Application.Exceptions; +using MethodologyMain.Application.Interface; +using MethodologyMain.Persistence.Interfaces; + +namespace MethodologyMain.Application.Services +{ + public class TeamValidationService : ITeamValidationService + { + public readonly ITeamRepository teamRepo; + public TeamValidationService(ITeamRepository teamRepo) + { + this.teamRepo = teamRepo; + } + + // Проверка существования команды + public async Task CheckTeamExistsAsync(Guid teamId, CancellationToken token) + { + if (!await teamRepo.CheckTeamExistAsync(teamId, token)) + { + throw new TeamNotFoundException(); + } + } + // Проверка, что пользователь не состоит в команде для данного хакатона + public async Task CheckUserNotInAnyTeamForHackathonAsync(Guid userId, Guid hackathonId, CancellationToken token) + { + if (await teamRepo.CheckUserTeamInHackAsync(userId, hackathonId, token)) + { + throw new MemberAlreadyInTeamException(); + } + } + + // Проверка, что пользователь не состоит в конкретной команде + public async Task CheckUserNotInTeamAsync(Guid userId, Guid teamId, CancellationToken token) + { + if (await teamRepo.CheckUserInTeamAsync(userId, teamId, token)) + { + throw new MemberAlreadyInTeamException(); + } + } + + // Проверка, что пользователь состоит в команде + public async Task CheckUserInTeamAsync(Guid userId, Guid teamId, CancellationToken token) + { + if (!await teamRepo.CheckUserInTeamAsync(userId, teamId, token)) + { + throw new UserNotInTeamException(); + } + } + + // Проверка, что пользователь имеет права капитана + public async Task CheckUserIsCaptainAsync(Guid teamId, Guid userId, CancellationToken token) + { + if (await teamRepo.GetCaptainIdAsync(teamId, token) != userId) + { + throw new UnauthorizedAccessException(); + } + } + public async Task CheckUserIsCaptainOrAdminAsync(Guid teamId, Guid userId, bool isAdmin, CancellationToken token) + { + if (await teamRepo.GetCaptainIdAsync(teamId, token) != userId && !isAdmin) + { + throw new UnauthorizedAccessException(); + } + } + public async Task CheckUserIsCaptainOrRequestedAsync(Guid teamId, Guid userId, Guid requestingUserId, CancellationToken token) + { + Guid captainId = (Guid)await teamRepo.GetCaptainIdAsync(teamId, token); + bool canRemove = (captainId == requestingUserId) || (userId == requestingUserId); + if (!canRemove) + { + throw new UnauthorizedAccessException(); + } + } + public async Task CheckCaptainKick(Guid teamId, Guid userId, CancellationToken token) + { + if (await teamRepo.GetCaptainIdAsync(teamId, token) == userId) + { + throw new InvalidOperationException("Капитан не может быть удален из команды. Сначала передайте права капитана другому участнику."); + } + } + } +} diff --git a/MethodologyMain.Infrastructure/DLL/RabbitMqListener.dll b/MethodologyMain.Infrastructure/DLL/RabbitMqListener.dll new file mode 100644 index 0000000..91b9c2c Binary files /dev/null and b/MethodologyMain.Infrastructure/DLL/RabbitMqListener.dll differ diff --git a/MethodologyMain.Infrastructure/DLL/RabbitMqModel.dll b/MethodologyMain.Infrastructure/DLL/RabbitMqModel.dll new file mode 100644 index 0000000..bbf46e2 Binary files /dev/null and b/MethodologyMain.Infrastructure/DLL/RabbitMqModel.dll differ diff --git a/MethodologyMain.Infrastructure/DLL/RabbitMqPublisher.dll b/MethodologyMain.Infrastructure/DLL/RabbitMqPublisher.dll new file mode 100644 index 0000000..114d943 Binary files /dev/null and b/MethodologyMain.Infrastructure/DLL/RabbitMqPublisher.dll differ diff --git a/MethodologyMain.Infrastructure/Listeners/RabbitMqUserRegisterListener.cs b/MethodologyMain.Infrastructure/Listeners/RabbitMqUserRegisterListener.cs new file mode 100644 index 0000000..70fea0b --- /dev/null +++ b/MethodologyMain.Infrastructure/Listeners/RabbitMqUserRegisterListener.cs @@ -0,0 +1,48 @@ +using AuthMetodology.Infrastructure.Models; +using MethodologyMain.Infrastructure.Models; +using MethodologyMain.Logic.Entities; +using MethodologyMain.Persistence.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using RabbitMqListener.Abstract; +using RabbitMqModel.Models; +using RabbitMqPublisher.Interface; +using Serilog.Events; +using System.Text.Json; + +namespace MethodologyMain.Infrastructure.Listeners +{ + public class RabbitMqUserRegisterListener : RabbitMqListenerBase + { + private readonly IServiceProvider serviceProvider; + private readonly IRabbitMqPublisherBase logPublishService; + public RabbitMqUserRegisterListener(IRabbitMqPublisherBase logPublishService, IServiceProvider serviceProvider, IOptions options) : base(options) + { + this.logPublishService = logPublishService; + this.serviceProvider = serviceProvider; + } + + protected override string QueueName => "RegisterUserQueue"; + + public override async Task ProcessMessageAsync(string message) + { + var data = JsonSerializer.Deserialize(message)!; + _ = logPublishService.SendEventAsync(new RabbitMqLogPublish + { + ServiceName = "Main service", + LogLevel = LogEventLevel.Information, + Message = "Method ProcessMessageAsync to register a user was called", + TimeStamp = DateTime.UtcNow + }); + using (var scope = serviceProvider.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + await context.AddAsync(new UserMainEntity + { + Id = data.UserId + }); + } + + } + } +} diff --git a/MethodologyMain.Infrastructure/MethodologyMain.Infrastructure.csproj b/MethodologyMain.Infrastructure/MethodologyMain.Infrastructure.csproj index 2150e37..78f38d2 100644 --- a/MethodologyMain.Infrastructure/MethodologyMain.Infrastructure.csproj +++ b/MethodologyMain.Infrastructure/MethodologyMain.Infrastructure.csproj @@ -1,10 +1,40 @@  - Exe + Library net8.0 enable enable + + + + + + + + + + + + + + + + + + + + + DLL\RabbitMqListener.dll + + + DLL\RabbitMqModel.dll + + + DLL\RabbitMqPublisher.dll + + + diff --git a/MethodologyMain.Infrastructure/Models/JWTOptions.cs b/MethodologyMain.Infrastructure/Models/JWTOptions.cs new file mode 100644 index 0000000..7e45715 --- /dev/null +++ b/MethodologyMain.Infrastructure/Models/JWTOptions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MethodologyMain.Infrastructure.Models +{ + public class JWTOptions + { + public required string SecurityKey { get; set; } + } +} diff --git a/MethodologyMain.Infrastructure/Models/RabbitMqLogPublish.cs b/MethodologyMain.Infrastructure/Models/RabbitMqLogPublish.cs new file mode 100644 index 0000000..646bd5f --- /dev/null +++ b/MethodologyMain.Infrastructure/Models/RabbitMqLogPublish.cs @@ -0,0 +1,15 @@ +using Serilog.Events; + +namespace AuthMetodology.Infrastructure.Models +{ + public class RabbitMqLogPublish + { + public required string Message { get; set; } + + public DateTime TimeStamp { get; set; } = DateTime.UtcNow; + + public required LogEventLevel LogLevel { get; set; } + + public required string ServiceName { get; set; } + } +} diff --git a/MethodologyMain.Infrastructure/Models/RabbitMqUserRegisterRecieve.cs b/MethodologyMain.Infrastructure/Models/RabbitMqUserRegisterRecieve.cs new file mode 100644 index 0000000..6c87699 --- /dev/null +++ b/MethodologyMain.Infrastructure/Models/RabbitMqUserRegisterRecieve.cs @@ -0,0 +1,11 @@ +using RabbitMqModel.Models; + +namespace MethodologyMain.Infrastructure.Models +{ + public class RabbitMqUserRegisterRecieve : IEvent + { + public Guid UserId { get; set; } + + public string EventType => "RecieveUserRegister"; + } +} diff --git a/MethodologyMain.Infrastructure/Services/LogQueueService.cs b/MethodologyMain.Infrastructure/Services/LogQueueService.cs new file mode 100644 index 0000000..16f77f5 --- /dev/null +++ b/MethodologyMain.Infrastructure/Services/LogQueueService.cs @@ -0,0 +1,19 @@ +using AuthMetodology.Infrastructure.Models; +using Microsoft.Extensions.Options; +using RabbitMqModel.Models; +using RabbitMqPublisher.Abstract; + +namespace MethodologyMain.Infrastructure.Services +{ + public class LogQueueService : RabbitMqPublisherBase + { + public LogQueueService(IOptions options) : base(options) { } + + public override string QueueName => "LogQueue"; + + public override async Task SendEventAsync(RabbitMqLogPublish message, CancellationToken cancellationToken = default) + { + await SendMessageAsync(message, QueueName, cancellationToken); + } + } +} diff --git a/MethodologyMain.Logic/Entities/UserMainEntity.cs b/MethodologyMain.Logic/Entities/UserMainEntity.cs index 5a06803..10d3b2e 100644 --- a/MethodologyMain.Logic/Entities/UserMainEntity.cs +++ b/MethodologyMain.Logic/Entities/UserMainEntity.cs @@ -10,7 +10,7 @@ public class UserMainEntity [Column("id")] public required Guid Id { get; set; } [Column("birthDate")] - public DateTime BirthDate { get; set; } + public DateTime BirthDate { get; set; } = DateTime.MinValue; [Column("education")] public string Education { get; set; } = string.Empty; [Column("firstName")] diff --git a/MethodologyMain.Logic/MethodologyMain.Logic.csproj b/MethodologyMain.Logic/MethodologyMain.Logic.csproj index 2150e37..412d83d 100644 --- a/MethodologyMain.Logic/MethodologyMain.Logic.csproj +++ b/MethodologyMain.Logic/MethodologyMain.Logic.csproj @@ -1,7 +1,7 @@  - Exe + Library net8.0 enable enable diff --git a/MethodologyMain.Logic/Models/UserMain.cs b/MethodologyMain.Logic/Models/UserMain.cs index 7982adc..3ba043f 100644 --- a/MethodologyMain.Logic/Models/UserMain.cs +++ b/MethodologyMain.Logic/Models/UserMain.cs @@ -13,7 +13,7 @@ namespace MethodologyMain.Logic.Models public class UserMain { public required Guid Id { get; set; } - public DateTime BirthDate { get; set; } + public DateTime BirthDate { get; set; } = DateTime.MinValue; public string Education { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; diff --git a/MethodologyMain.Persistence/Interfaces/IGenericRepository.cs b/MethodologyMain.Persistence/Interfaces/IGenericRepository.cs index cf8e8b0..22d0c63 100644 --- a/MethodologyMain.Persistence/Interfaces/IGenericRepository.cs +++ b/MethodologyMain.Persistence/Interfaces/IGenericRepository.cs @@ -4,12 +4,12 @@ namespace MethodologyMain.Persistence.Interfaces { public interface IGenericRepository where T : class { - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task> FindAsync(Expression> expression); - Task AddAsync(T entity); - Task AddRangeAsync(IEnumerable entities); - Task RemoveAsync(T entity); - Task RemoveRangeAsync(IEnumerable entities); + Task AddAsync(T entity, CancellationToken token = default); + Task AddRangeAsync(IEnumerable entities, CancellationToken token = default); + Task> FindAsync(Expression> expression, CancellationToken token = default); + Task> GetAllAsync(CancellationToken token = default); + Task GetByIdAsync(Guid id, CancellationToken token = default); + Task RemoveAsync(T entity, CancellationToken token = default); + Task RemoveRangeAsync(IEnumerable entities, CancellationToken token = default); } } diff --git a/MethodologyMain.Persistence/Interfaces/ITeamRepository.cs b/MethodologyMain.Persistence/Interfaces/ITeamRepository.cs index d379d45..ed7d7b7 100644 --- a/MethodologyMain.Persistence/Interfaces/ITeamRepository.cs +++ b/MethodologyMain.Persistence/Interfaces/ITeamRepository.cs @@ -1,20 +1,19 @@ using MethodologyMain.Logic.Entities; -using MethodTeams.Models; namespace MethodologyMain.Persistence.Interfaces { public interface ITeamRepository : IGenericRepository { - Task AddMemberAsync(Guid userId, Guid teamId); - Task CheckUserInTeamAsync(Guid userId, Guid teamId); - Task CheckUserTeamInHackAsync(Guid userId, Guid hackathonId); - Task CheckTeamExistAsync(Guid teamId); - Task RemoveTeamAsync(Guid teamId); - Task RemoveMemberAsync(Guid userId, Guid teamId); - Task TransferCaptainAsync(Guid newCaptainId, Guid teamId); - Task GetCaptainIdAsync(Guid teamId); - Task GetHackathonIdAsync(Guid teamId); - Task> GetTeamMembersAsync(Guid teamId); + Task AddMemberAsync(Guid userId, Guid teamId, CancellationToken token); + Task CheckTeamExistAsync(Guid teamId, CancellationToken token); + Task CheckUserInTeamAsync(Guid userId, Guid teamId, CancellationToken token); + Task CheckUserTeamInHackAsync(Guid userId, Guid hackathonId, CancellationToken token); + Task GetCaptainIdAsync(Guid teamId, CancellationToken token); + Task GetHackathonIdAsync(Guid teamId, CancellationToken token); + Task?> GetTeamMembersAsync(Guid teamId, CancellationToken token); + Task RemoveMemberAsync(Guid userId, Guid teamId, CancellationToken token); + Task RemoveTeamAsync(Guid teamId, CancellationToken token); + Task TransferCaptainAsync(Guid newCaptainId, Guid teamId, CancellationToken token); } } diff --git a/MethodologyMain.Persistence/Interfaces/IUserRepository.cs b/MethodologyMain.Persistence/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..a53ee09 --- /dev/null +++ b/MethodologyMain.Persistence/Interfaces/IUserRepository.cs @@ -0,0 +1,13 @@ +using MethodologyMain.Logic.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MethodologyMain.Persistence.Interfaces +{ + public interface IUserRepository : IGenericRepository + { + } +} diff --git a/MethodologyMain.Persistence/MethodologyMain.Persistence.csproj b/MethodologyMain.Persistence/MethodologyMain.Persistence.csproj index 86db8d8..0dbfb18 100644 --- a/MethodologyMain.Persistence/MethodologyMain.Persistence.csproj +++ b/MethodologyMain.Persistence/MethodologyMain.Persistence.csproj @@ -1,18 +1,27 @@  - Exe + Library net8.0 enable enable - + - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/MethodologyMain.Persistence/Repository/GenericRepository.cs b/MethodologyMain.Persistence/Repository/GenericRepository.cs index a22be42..8b57920 100644 --- a/MethodologyMain.Persistence/Repository/GenericRepository.cs +++ b/MethodologyMain.Persistence/Repository/GenericRepository.cs @@ -13,43 +13,43 @@ public GenericRepository(MyDbContext context) _context = context; } - public async Task AddAsync(T entity) + public async Task AddAsync(T entity, CancellationToken token) { - await _context.Set().AddAsync(entity); - await _context.SaveChangesAsync(); + _context.Set().Add(entity); + await _context.SaveChangesAsync(token); } - public async Task AddRangeAsync(IEnumerable entities) + public async Task AddRangeAsync(IEnumerable entities, CancellationToken token) { - await _context.Set().AddRangeAsync(entities); - await _context.SaveChangesAsync(); + _context.Set().AddRange(entities); + await _context.SaveChangesAsync(token); } - public async Task> FindAsync(Expression> expression) + public async Task> FindAsync(Expression> expression, CancellationToken token) { - return await _context.Set().Where(expression).ToListAsync(); + return await _context.Set().Where(expression).ToListAsync(token); } - public async Task> GetAllAsync() + public async Task> GetAllAsync(CancellationToken token) { - return await _context.Set().ToListAsync(); + return await _context.Set().ToListAsync(token); } - public async Task GetByIdAsync(Guid id) + public async Task GetByIdAsync(Guid id, CancellationToken token) { - return await _context.Set().FindAsync(id); + return await _context.Set().FindAsync([id], token); } - public async Task RemoveAsync(T entity) + public async Task RemoveAsync(T entity, CancellationToken token) { _context.Set().Remove(entity); - await _context.SaveChangesAsync(); + await _context.SaveChangesAsync(token); } - public async Task RemoveRangeAsync(IEnumerable entities) + public async Task RemoveRangeAsync(IEnumerable entities, CancellationToken token) { _context.Set().RemoveRange(entities); - await _context.SaveChangesAsync(); + await _context.SaveChangesAsync(token); } } } diff --git a/MethodologyMain.Persistence/Repository/TeamRepository.cs b/MethodologyMain.Persistence/Repository/TeamRepository.cs index 02b9146..0ea85d4 100644 --- a/MethodologyMain.Persistence/Repository/TeamRepository.cs +++ b/MethodologyMain.Persistence/Repository/TeamRepository.cs @@ -1,10 +1,8 @@ using MethodologyMain.Logic.Entities; using MethodologyMain.Persistence.Interfaces; using MethodTeams.Data; -using MethodTeams.Models; using Microsoft.EntityFrameworkCore; - namespace MethodologyMain.Persistence.Repository { public class TeamRepository : GenericRepository, ITeamRepository @@ -12,87 +10,91 @@ public class TeamRepository : GenericRepository, ITeamRepository public TeamRepository(MyDbContext context) : base(context) { } - public async Task CheckUserTeamInHackAsync(Guid userId, Guid hackathonId) + public async Task CheckUserTeamInHackAsync(Guid userId, Guid hackathonId, CancellationToken token) { - // User cannot be null - var user = await _context.Users. - AsNoTracking(). - FirstAsync(e => e.Id == userId); - return user.Teams.Find(m => m.Team.HackathonId == hackathonId) - != null; + CheckCancellation(token); + var user = await _context.Users + .AsNoTracking() + .FirstAsync(e => e.Id == userId, token); + return user.Teams.Find(m => m.Team.HackathonId == hackathonId) != null; } - public async Task CheckUserInTeamAsync(Guid userId, Guid teamId) + public async Task CheckUserInTeamAsync(Guid userId, Guid teamId, CancellationToken token) { - // Team cannot be null checked before - var team = await _context.Teams. - AsNoTracking(). - FirstAsync(e => e.Id == teamId); - return team.Members.Find(m => m.UserId == userId) - != null; + var team = await GetTeamNoTrackingAsync(teamId, token); + return team.Members.Find(m => m.UserId == userId) != null; } - public async Task CheckTeamExistAsync(Guid teamId) + public async Task CheckTeamExistAsync(Guid teamId, CancellationToken token) { - // Team cannot be null checked before - var team = await _context.Teams. - AsNoTracking(). - FirstAsync(e => e.Id == teamId); + var team = await GetTeamNoTrackingAsync(teamId, token); return team != null; } - public async Task AddMemberAsync(Guid userId, Guid teamId) + public async Task AddMemberAsync(Guid userId, Guid teamId, CancellationToken token) { - // Team cannot be null checked before - var team = await _context.Teams.FindAsync(teamId); + var team = await GetTeamAsync(teamId, token); team.Members.Add(new UserTeamEntity { TeamId = teamId, UserId = userId, JoinedAt = DateTime.UtcNow }); - await _context.SaveChangesAsync(); + await SaveChangesAsync(token); } - public async Task RemoveTeamAsync(Guid teamId) + public async Task RemoveTeamAsync(Guid teamId, CancellationToken token) { - // Удаление связанных участников (maybe not required) - var team = await _context.Teams.FindAsync(teamId); + var team = await GetTeamAsync(teamId, token); if (team.Members is not null) { var _ = team.Members.RemoveAll; } - // Удаление команды _context.Teams.Remove(team); - await _context.SaveChangesAsync(); + await SaveChangesAsync(token); } - - public async Task RemoveMemberAsync(Guid userId, Guid teamId) + public async Task RemoveMemberAsync(Guid userId, Guid teamId, CancellationToken token) { - var team = await _context.Teams.FindAsync(teamId); + var team = await GetTeamAsync(teamId, token); var member = team.Members.Find(e => e.UserId == userId); team.Members.Remove(member); - await _context.SaveChangesAsync(); + await SaveChangesAsync(token); } - - public async Task TransferCaptainAsync(Guid newCaptainId, Guid teamId) + public async Task TransferCaptainAsync(Guid newCaptainId, Guid teamId, CancellationToken token) { - var team = await _context.Teams.FindAsync(teamId); + var team = await GetTeamAsync(teamId, token); var member = team.Members.Find(e => e.UserId == newCaptainId); team.CaptainId = newCaptainId; - await _context.SaveChangesAsync(); + await SaveChangesAsync(token); } - public async Task GetCaptainIdAsync(Guid teamId) + public async Task GetCaptainIdAsync(Guid teamId, CancellationToken token) { - var team = await _context.Teams. - AsNoTracking(). - FirstAsync(e => e.Id == teamId); + var team = await GetTeamNoTrackingAsync(teamId, token); return team.CaptainId; } - public async Task GetHackathonIdAsync(Guid teamId) + public async Task GetHackathonIdAsync(Guid teamId, CancellationToken token) { - var team = await _context.Teams. - AsNoTracking(). - FirstAsync(e => e.Id == teamId); + var team = await GetTeamNoTrackingAsync(teamId, token); return team.HackathonId; } - public async Task> GetTeamMembersAsync(Guid teamId) + public async Task?> GetTeamMembersAsync(Guid teamId, CancellationToken token) { - var team = await _context.Teams.FindAsync(teamId); + var team = await GetTeamAsync(teamId, token); return team.Members.Select(m => m.UserId).ToList(); } + private static void CheckCancellation(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + } + private async Task GetTeamNoTrackingAsync(Guid teamId, CancellationToken token) + { + CheckCancellation(token); + return await _context.Teams + .AsNoTracking() + .FirstAsync(e => e.Id == teamId, token); + } + private async Task GetTeamAsync(Guid teamId, CancellationToken token) + { + CheckCancellation(token); + return await _context.Teams.FindAsync([teamId], token); + } + private async Task SaveChangesAsync(CancellationToken token) + { + CheckCancellation(token); + await _context.SaveChangesAsync(token); + } } -} +} \ No newline at end of file diff --git a/MethodologyMain.Persistence/Repository/UserRepository.cs b/MethodologyMain.Persistence/Repository/UserRepository.cs new file mode 100644 index 0000000..8bb81b2 --- /dev/null +++ b/MethodologyMain.Persistence/Repository/UserRepository.cs @@ -0,0 +1,15 @@ +using MethodologyMain.Logic.Entities; +using MethodologyMain.Persistence.Interfaces; +using MethodTeams.Data; + +namespace MethodologyMain.Persistence.Repository +{ + public class UserRepository: GenericRepository, IUserRepository + { + public UserRepository(MyDbContext context) : base(context) + { + } + + + } +} diff --git a/MethodologyMain.sln b/MethodologyMain.sln new file mode 100644 index 0000000..f45ff41 --- /dev/null +++ b/MethodologyMain.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35825.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MethodologyMain.Persistence", "MethodologyMain.Persistence\MethodologyMain.Persistence.csproj", "{1FC82515-12C6-4FEE-BBD1-7B8F8556FFCC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MethodologyMain.Application", "MethodologyMain.Application\MethodologyMain.Application.csproj", "{690F1F05-174F-4BB9-97DF-9B6BB9109AA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MethodologyMain.Logic", "MethodologyMain.Logic\MethodologyMain.Logic.csproj", "{72684002-8EB1-4F55-92ED-B255F3632320}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MethodologyMain.Infrastructure", "MethodologyMain.Infrastructure\MethodologyMain.Infrastructure.csproj", "{BAB29F8B-543F-4F66-9D4C-3438ACCBEBEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MethodologyMain.API", "MethodologyMain.API\MethodologyMain.API.csproj", "{9617192C-0310-6F26-DC5C-04497DB93B87}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1FC82515-12C6-4FEE-BBD1-7B8F8556FFCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FC82515-12C6-4FEE-BBD1-7B8F8556FFCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FC82515-12C6-4FEE-BBD1-7B8F8556FFCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FC82515-12C6-4FEE-BBD1-7B8F8556FFCC}.Release|Any CPU.Build.0 = Release|Any CPU + {690F1F05-174F-4BB9-97DF-9B6BB9109AA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {690F1F05-174F-4BB9-97DF-9B6BB9109AA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {690F1F05-174F-4BB9-97DF-9B6BB9109AA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {690F1F05-174F-4BB9-97DF-9B6BB9109AA7}.Release|Any CPU.Build.0 = Release|Any CPU + {72684002-8EB1-4F55-92ED-B255F3632320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72684002-8EB1-4F55-92ED-B255F3632320}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72684002-8EB1-4F55-92ED-B255F3632320}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72684002-8EB1-4F55-92ED-B255F3632320}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB29F8B-543F-4F66-9D4C-3438ACCBEBEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB29F8B-543F-4F66-9D4C-3438ACCBEBEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB29F8B-543F-4F66-9D4C-3438ACCBEBEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB29F8B-543F-4F66-9D4C-3438ACCBEBEE}.Release|Any CPU.Build.0 = Release|Any CPU + {9617192C-0310-6F26-DC5C-04497DB93B87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9617192C-0310-6F26-DC5C-04497DB93B87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9617192C-0310-6F26-DC5C-04497DB93B87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9617192C-0310-6F26-DC5C-04497DB93B87}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A05B680E-2581-4C1F-B1A8-71A587C941CF} + EndGlobalSection +EndGlobal