diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a91ffed..68e56a7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,3 +22,5 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore
+ - name: Test
+ run: dotnet test --no-build
diff --git a/StudHub.Server.sln b/StudHub.Server.sln
index ec49472..f1e5bae 100644
--- a/StudHub.Server.sln
+++ b/StudHub.Server.sln
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platform.API", "src\Platfor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.API", "src\Admin.API\Admin.API.csproj", "{781F6E4E-FC3B-46E4-895A-F27A4310CF42}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unit", "test\Unit\Unit.csproj", "{3558A138-C00A-4857-93E5-3ECCB5CD873C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +38,9 @@ Global
{781F6E4E-FC3B-46E4-895A-F27A4310CF42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{781F6E4E-FC3B-46E4-895A-F27A4310CF42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{781F6E4E-FC3B-46E4-895A-F27A4310CF42}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3558A138-C00A-4857-93E5-3ECCB5CD873C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3558A138-C00A-4857-93E5-3ECCB5CD873C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3558A138-C00A-4857-93E5-3ECCB5CD873C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3558A138-C00A-4857-93E5-3ECCB5CD873C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/src/Admin.API/Admin.API.csproj b/src/Admin.API/Admin.API.csproj
index a16c169..baf4193 100644
--- a/src/Admin.API/Admin.API.csproj
+++ b/src/Admin.API/Admin.API.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Admin.API/Controllers/ReportController.cs b/src/Admin.API/Controllers/ReportController.cs
new file mode 100644
index 0000000..e96d329
--- /dev/null
+++ b/src/Admin.API/Controllers/ReportController.cs
@@ -0,0 +1,49 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Application;
+using Application.DTO;
+using Application.Interfaces.Queries;
+using Application.Interfaces.Services;
+using Domain.Entities.Admin;
+
+namespace Admin.API.Controllers;
+
+[Authorize]
+[ApiController]
+[Route("reports")]
+public class ReportController : ControllerBase
+{
+ private readonly IReportsService _reportsService;
+ private readonly IReportsQueryService _reportsQueryService;
+
+ public ReportController(IReportsService reportsService, IReportsQueryService reportsQueryService)
+ {
+ _reportsService = reportsService;
+ _reportsQueryService = reportsQueryService;
+ }
+
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task Create(ReportDTO reportDTO)
+ {
+ await _reportsService.CreateReport(reportDTO);
+ return new StatusCodeResult(StatusCodes.Status201Created);
+ }
+
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetAll() => Ok(await _reportsQueryService.GetAllAsync());
+
+ [HttpGet("{id:int}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task> GetById(int id) => Ok(await _reportsQueryService.GetByIdAsync(id));
+
+ [HttpGet("getByTargetEntityId")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task>> GetByTargetEntityId(ReportTargetEntities entity)
+ => Ok(await _reportsQueryService.GetByTargetEntityIdAsync(entity));
+}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/AuthorizationExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
similarity index 62%
rename from src/Admin.API/ExceptionHandlers/AuthorizationExceptionHandler.cs
rename to src/Admin.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
index 9d294e2..9c1c74e 100644
--- a/src/Admin.API/ExceptionHandlers/AuthorizationExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
@@ -1,16 +1,25 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using Application.Exceptions;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
-public class AuthorizationExceptionHandler : IExceptionHandler
+public class ApplicationAuthorizationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Доступ запрещён";
private const int StatusCode = StatusCodes.Status403Forbidden;
+ private readonly ILogger _logger;
+
+ public ApplicationAuthorizationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
- if (exception is not UnauthorizedAccessException)
+ if (exception is not ApplicationAuthorizationException)
{
return false;
}
@@ -23,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/AuthenticationExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
index 24b67fa..55069a5 100644
--- a/src/Admin.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
@@ -1,6 +1,7 @@
using System.Security.Authentication;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class AuthenticationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "При аутентификации произошла ошибка";
private const int StatusCode = StatusCodes.Status401Unauthorized;
+ private readonly ILogger _logger;
+
+ public AuthenticationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/BadRequestExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/BadRequestExceptionHandler.cs
index d850610..6417cfa 100644
--- a/src/Admin.API/ExceptionHandlers/BadRequestExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/BadRequestExceptionHandler.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
@@ -7,6 +8,13 @@ public class BadRequestExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Некорректный запрос";
private const int StatusCode = StatusCodes.Status400BadRequest;
+ private readonly ILogger _logger;
+
+ public BadRequestExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -23,6 +31,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/GlobalExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/GlobalExceptionHandler.cs
index 2cba8d8..eb4df35 100644
--- a/src/Admin.API/ExceptionHandlers/GlobalExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/GlobalExceptionHandler.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
@@ -7,6 +8,13 @@ public class GlobalExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Произошла ошибка на стороне сервера";
private const int StatusCode = StatusCodes.Status500InternalServerError;
+ private readonly ILogger _logger;
+
+ public GlobalExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -19,6 +27,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Error(exception, ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
index cfa7cb1..bbab133 100644
--- a/src/Admin.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class InvalidOperationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Недопустимая операция";
private const int StatusCode = StatusCodes.Status409Conflict;
+ private readonly ILogger _logger;
+
+ public InvalidOperationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/ExceptionHandlers/NotFoundExceptionHandler.cs b/src/Admin.API/ExceptionHandlers/NotFoundExceptionHandler.cs
index 5a95095..ab2ee88 100644
--- a/src/Admin.API/ExceptionHandlers/NotFoundExceptionHandler.cs
+++ b/src/Admin.API/ExceptionHandlers/NotFoundExceptionHandler.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Application.Exceptions;
+using ILogger = Serilog.ILogger;
namespace Admin.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class NotFoundExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Запрашиваемый ресурс отсутствует на сервере";
private const int StatusCode = StatusCodes.Status404NotFound;
+ private readonly ILogger _logger;
+
+ public NotFoundExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Admin.API/Extensions/ServiceCollectionExtensions.cs b/src/Admin.API/Extensions/ServiceCollectionExtensions.cs
index b14f38a..ca65408 100644
--- a/src/Admin.API/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Admin.API/Extensions/ServiceCollectionExtensions.cs
@@ -10,7 +10,7 @@ public static IServiceCollection AddExceptionHandlers(this IServiceCollection se
services.AddExceptionHandler();
services.AddExceptionHandler();
services.AddExceptionHandler();
- services.AddExceptionHandler();
+ services.AddExceptionHandler();
services.AddExceptionHandler();
return services;
}
diff --git a/src/Admin.API/Program.cs b/src/Admin.API/Program.cs
index 8ee4202..b2ca618 100644
--- a/src/Admin.API/Program.cs
+++ b/src/Admin.API/Program.cs
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
+using Serilog;
+using Serilog.Filters;
using Application.Interfaces.Services;
using Infrastructure.Extensions;
using Admin.API.Extensions;
@@ -12,8 +14,18 @@ public class Program
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
+ builder.Host.UseSerilog((context, configuration) =>
+ {
+ configuration.ReadFrom.Configuration(context.Configuration);
+ // Временное решение, дублирование логов при использовании ExceptionHandlerMiddleware исправлено в .NET 10
+ configuration.Filter.ByExcluding(Matching
+ .FromSource());
+ });
builder.Services.AddEndpointsApiExplorer();
- builder.Services.AddOpenApiDocument();
+ builder.Services.AddOpenApiDocument(options =>
+ {
+ options.Title = builder.Environment.IsDevelopment() ? "StudHub Admin API [DEV]" : "StudHub Admin API";
+ });
builder.Configuration.AddEnvironmentVariables();
builder.Services.AddStudHubServices(builder.Configuration.GetConnectionString("Default")!);
builder.Services.AddAdminServices();
@@ -67,6 +79,7 @@ public static void Main(string[] args)
};
});
var app = builder.Build();
+ app.UseSerilogRequestLogging();
app.UseCors();
app.UseExceptionHandler();
app.UseOpenApi(options =>
@@ -75,10 +88,12 @@ public static void Main(string[] args)
});
app.MapScalarApiReference(options =>
{
- options.Title = "StudHub.Admin API";
+ options.Title = app.Environment.IsDevelopment()
+ ? "StudHub Admin API [DEV]"
+ : "StudHub Admin API";
options.Servers = new List
{
- new ScalarServer("https://api.admin.setka-rtu.ru")
+ new ScalarServer("https://api-admin.setka-rtu.ru")
};
if (app.Environment.IsDevelopment())
{
@@ -90,4 +105,4 @@ public static void Main(string[] args)
app.Run();
}
-}
\ No newline at end of file
+}
diff --git a/src/Admin.API/appsettings.Development.json b/src/Admin.API/appsettings.Development.json
index 0c208ae..8fc50b9 100644
--- a/src/Admin.API/appsettings.Development.json
+++ b/src/Admin.API/appsettings.Development.json
@@ -1,8 +1,22 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft.EntityFrameworkCore": "Information",
+ "Microsoft.AspNetCore": "Information"
+ }
+ },
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "/Logs/Admin.API/Development/DevLog-.txt",
+ "rollingInterval": "Day"
+ }
+ }
+ ]
}
}
diff --git a/src/Admin.API/appsettings.json b/src/Admin.API/appsettings.json
index 7b0e17a..1d121cd 100644
--- a/src/Admin.API/appsettings.json
+++ b/src/Admin.API/appsettings.json
@@ -1,14 +1,24 @@
{
- "Logging": {
- "LogLevel": {
+ "Serilog": {
+ "MinimumLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "Override": {
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "/Logs/Admin.API/Production/Log-.txt",
+ "rollingInterval": "Day"
+ }
+ }
+ ]
},
"Domain": "setka-rtu.ru",
- "AllowedHosts": "*",
- "JWTKey": "9>I4=o8{AG\\Nh{`I7m*={tP:Gs)dsXxNw.Hj92]dQSO",
- "ConnectionStrings": {
- "Default": "Host=31.128.38.159;Port=5432;User ID=admin-setka;Password=P1{Hg?:(~jLfDNrp!W9QkF?bO;Database=SetkaDB"
- }
+ "AllowedHosts": "*"
}
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
index d1ca462..a262b2f 100644
--- a/src/Application/Application.csproj
+++ b/src/Application/Application.csproj
@@ -12,6 +12,9 @@
+
+
+
diff --git a/src/Application/DTO/ClubCreationRequestDTO.cs b/src/Application/DTO/ClubCreationRequestDTO.cs
index f94ba30..ab4c3e5 100644
--- a/src/Application/DTO/ClubCreationRequestDTO.cs
+++ b/src/Application/DTO/ClubCreationRequestDTO.cs
@@ -27,8 +27,8 @@ public record ClubCreationRequestDetailsDTO
public string ClubName { get; init; }
public string? ClubAbout { get; init; }
[Required]
- public int ImageId { get; init; }
- public int? BannerId { get; init; }
+ public string ImagePath { get; init; }
+ public string? BannerPath { get; init; }
[Required]
public bool IsActive { get; init; }
[Required]
diff --git a/src/Application/DTO/PostDTO.cs b/src/Application/DTO/PostDTO.cs
index de0bf82..74f7392 100644
--- a/src/Application/DTO/PostDTO.cs
+++ b/src/Application/DTO/PostDTO.cs
@@ -12,7 +12,9 @@ public record PostDTO
[MaxLength(500)]
public required string Content { get; init; }
[Required]
- public int? ClubId { get; set; }
+ public int? ClubId { get; init; }
+ [MaxLength(5)]
+ public List PostImagesIds { get; init; } = new List();
}
public record PostDetailDTO
diff --git a/src/Application/DTO/ReportDTO.cs b/src/Application/DTO/ReportDTO.cs
new file mode 100644
index 0000000..65e3fdb
--- /dev/null
+++ b/src/Application/DTO/ReportDTO.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Application.DTO;
+
+public record ReportDTO
+{
+ [Required]
+ public int? TargetId { get; init; }
+ [Required]
+ public int? TargetEntityId { get; init; }
+ [Required]
+ public int? CategoryId { get; init; }
+ public string? Description { get; init; }
+}
\ No newline at end of file
diff --git a/src/Application/Enums/ReportTargetEntities.cs b/src/Application/Enums/ReportTargetEntities.cs
new file mode 100644
index 0000000..082197a
--- /dev/null
+++ b/src/Application/Enums/ReportTargetEntities.cs
@@ -0,0 +1,8 @@
+namespace Application;
+
+public enum ReportTargetEntities
+{
+ Person = 1,
+ Club = 2,
+ Post = 3
+}
\ No newline at end of file
diff --git a/src/Application/Exceptions/ApplicationAuthorizationException.cs b/src/Application/Exceptions/ApplicationAuthorizationException.cs
new file mode 100644
index 0000000..862c995
--- /dev/null
+++ b/src/Application/Exceptions/ApplicationAuthorizationException.cs
@@ -0,0 +1,3 @@
+namespace Application.Exceptions;
+
+public class ApplicationAuthorizationException(string? message) : UnauthorizedAccessException(message);
\ No newline at end of file
diff --git a/src/Application/Interfaces/Queries/IReportsQueryService.cs b/src/Application/Interfaces/Queries/IReportsQueryService.cs
new file mode 100644
index 0000000..4b416e2
--- /dev/null
+++ b/src/Application/Interfaces/Queries/IReportsQueryService.cs
@@ -0,0 +1,10 @@
+using Domain.Entities.Admin;
+
+namespace Application.Interfaces.Queries;
+
+public interface IReportsQueryService
+{
+ public Task> GetAllAsync();
+ public Task GetByIdAsync(int id);
+ public Task> GetByTargetEntityIdAsync(ReportTargetEntities entity);
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/Repositories/Admin/IReportCategoriesRepository.cs b/src/Application/Interfaces/Repositories/Admin/IReportCategoriesRepository.cs
new file mode 100644
index 0000000..ecc9d66
--- /dev/null
+++ b/src/Application/Interfaces/Repositories/Admin/IReportCategoriesRepository.cs
@@ -0,0 +1,9 @@
+using Domain.Entities.Admin;
+
+namespace Application.Interfaces.Repositories.Admin;
+
+public interface IReportCategoriesRepository : IRepository
+{
+ public Task GetByIdAsync(int id);
+ public Task CreateAsync(ReportCategory reportCategory);
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/Repositories/Admin/IReportEntitiesRepository.cs b/src/Application/Interfaces/Repositories/Admin/IReportEntitiesRepository.cs
new file mode 100644
index 0000000..70bbc5d
--- /dev/null
+++ b/src/Application/Interfaces/Repositories/Admin/IReportEntitiesRepository.cs
@@ -0,0 +1,8 @@
+using Domain.Entities.Admin;
+
+namespace Application.Interfaces.Repositories.Admin;
+
+public interface IReportEntitiesRepository : IRepository
+{
+ public Task GetByIdAsync(int id);
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/Repositories/Admin/IReportsRepository.cs b/src/Application/Interfaces/Repositories/Admin/IReportsRepository.cs
new file mode 100644
index 0000000..16e3940
--- /dev/null
+++ b/src/Application/Interfaces/Repositories/Admin/IReportsRepository.cs
@@ -0,0 +1,10 @@
+using Domain.Entities.Admin;
+
+namespace Application.Interfaces.Repositories.Admin;
+
+public interface IReportsRepository : IRepository
+{
+ public Task GetByIdAsync(int id);
+ public Task CreateAsync(Report report);
+ public Task UpdateAsync(Report report);
+}
\ No newline at end of file
diff --git a/src/Application/Interfaces/Services/IReportsService.cs b/src/Application/Interfaces/Services/IReportsService.cs
new file mode 100644
index 0000000..bc86fa2
--- /dev/null
+++ b/src/Application/Interfaces/Services/IReportsService.cs
@@ -0,0 +1,8 @@
+using Application.DTO;
+
+namespace Application.Interfaces.Services;
+
+public interface IReportsService
+{
+ public Task CreateReport(ReportDTO report);
+}
\ No newline at end of file
diff --git a/src/Application/Services/ClubAdminsService.cs b/src/Application/Services/ClubAdminsService.cs
index a6a6a19..2b19cfb 100644
--- a/src/Application/Services/ClubAdminsService.cs
+++ b/src/Application/Services/ClubAdminsService.cs
@@ -1,4 +1,5 @@
-using Domain.Entities.Club;
+using Serilog;
+using Domain.Entities.Club;
using Domain.Entities.Person;
using Application.Exceptions;
using Application.Interfaces.Repositories.Club;
@@ -13,16 +14,18 @@ public class ClubAdminsService : IClubAdminsService
private readonly IPersonsRepository _personsRepository;
private readonly IClubsRepository _clubsRepository;
private readonly ISessionService _sessionService;
+ private readonly ILogger _logger;
public ClubAdminsService(IClubAdminsRepository clubAdminsRepository, IClubsRepository clubsRepository,
- ISessionService sessionService, IPersonsRepository personsRepository)
+ ISessionService sessionService, IPersonsRepository personsRepository, ILogger logger)
{
_clubAdminsRepository = clubAdminsRepository;
_clubsRepository = clubsRepository;
_sessionService = sessionService;
_personsRepository = personsRepository;
+ _logger = logger;
}
-
+
public async Task GrantAdminRights(int personId, int clubId)
{
var club = await _clubsRepository.GetByIdAsync(clubId);
@@ -32,11 +35,12 @@ public async Task GrantAdminRights(int personId, int clubId)
if (person is null)
throw new NotFoundException(nameof(PersonModel), personId);
if (_sessionService.PersonId != club.OwnerId)
- throw new UnauthorizedAccessException("Пользователь должен являться владельцем клуба, чтобы назначить администратора");
+ throw new ApplicationAuthorizationException("Пользователь должен являться владельцем клуба, чтобы назначить администратора");
if (await _clubAdminsRepository.CheckIfContainsAsync(personId, clubId) || club.OwnerId == personId)
throw new InvalidOperationException("Пользователь уже является администратором или владельцем данного клуба");
var clubAdmin = new ClubAdmin { PersonId = personId, ClubId = clubId };
await _clubAdminsRepository.CreateAsync(clubAdmin);
+ _logger.Information("Пользователь с Id ({PersonId}) назначен администратором клуба с Id ({ClubId})", personId, clubId);
}
public async Task RevokeAdminRights(int personId, int clubId)
@@ -48,11 +52,12 @@ public async Task RevokeAdminRights(int personId, int clubId)
if (person is null)
throw new NotFoundException(nameof(PersonModel), personId);
if (_sessionService.PersonId != club.OwnerId)
- throw new UnauthorizedAccessException("Пользователь должен являться владельцем клуба, чтобы разжаловать администратора");
+ throw new ApplicationAuthorizationException("Пользователь должен являться владельцем клуба, чтобы разжаловать администратора");
if (!await _clubAdminsRepository.CheckIfContainsAsync(personId, clubId))
throw new InvalidOperationException("Пользователь не является администратором данного клуба");
var id = (await _clubAdminsRepository.GetByClubIdAsync(clubId)).First(c => c.ClubId == clubId
&& c.PersonId == personId).Id;
await _clubAdminsRepository.DeleteByIdAsync(id);
+ _logger.Information("Пользователь с Id ({PersonId}) лишён прав администратора клуба с Id ({ClubId})", personId, clubId);
}
}
\ No newline at end of file
diff --git a/src/Application/Services/ClubCreationRequestsService.cs b/src/Application/Services/ClubCreationRequestsService.cs
index 4196238..b3f7913 100644
--- a/src/Application/Services/ClubCreationRequestsService.cs
+++ b/src/Application/Services/ClubCreationRequestsService.cs
@@ -1,4 +1,5 @@
using AutoMapper;
+using Serilog;
using Domain.Entities.Admin;
using Domain.Entities.Club;
using Application.DTO;
@@ -14,20 +15,23 @@ public class ClubCreationRequestsService : IClubCreationRequestsService
private readonly IClubCreationRequestsRepository _clubCreationRequestsRepository;
private readonly IClubsRepository _clubsRepository;
private readonly IMapper _mapper;
-
+ private readonly ILogger _logger;
+
public ClubCreationRequestsService(IClubCreationRequestsRepository clubCreationRequestsRepository,
- IClubsRepository clubsRepository, IMapper mapper)
+ IClubsRepository clubsRepository, IMapper mapper, ILogger logger)
{
_clubCreationRequestsRepository = clubCreationRequestsRepository;
_clubsRepository = clubsRepository;
_mapper = mapper;
+ _logger = logger;
}
-
+
public async Task CreateAsync(ClubCreationRequestDTO clubCreationRequestDTO)
{
var clubCreationRequest = _mapper.Map(clubCreationRequestDTO);
clubCreationRequest.IsActive = true;
await _clubCreationRequestsRepository.CreateAsync(clubCreationRequest);
+ _logger.Information("Добавлена заявка на создание клуба с Id ({ClubCreationRequestId})", clubCreationRequest.Id);
}
public async Task AcceptRequest(int requestId)
@@ -47,7 +51,9 @@ public async Task AcceptRequest(int requestId)
await _clubsRepository.CreateAsync(club);
clubCreationRequest.IsActive = false;
await _clubCreationRequestsRepository.UpdateAsync(clubCreationRequest);
+ _logger.Information("Заявка на создание клуба с Id ({RequestId}) одобрена", requestId);
}
+
public async Task DeclineRequest(int requestId)
{
var clubCreationRequest = await _clubCreationRequestsRepository.GetByIdAsync(requestId);
@@ -56,5 +62,6 @@ public async Task DeclineRequest(int requestId)
throw new InvalidOperationException("Заявка на создание клуба уже была рассмотрена");
clubCreationRequest.IsActive = false;
await _clubCreationRequestsRepository.UpdateAsync(clubCreationRequest);
+ _logger.Information("Заявка на создание клуба с Id ({RequestId}) отклонена", requestId);
}
}
\ No newline at end of file
diff --git a/src/Application/Services/ClubsService.cs b/src/Application/Services/ClubsService.cs
index 30a4643..36731a0 100644
--- a/src/Application/Services/ClubsService.cs
+++ b/src/Application/Services/ClubsService.cs
@@ -1,4 +1,5 @@
using AutoMapper;
+using Serilog;
using Domain.Entities.Club;
using Application.DTO;
using Application.Exceptions;
@@ -13,16 +14,18 @@ public class ClubsService : IClubsService
private readonly IPersonsClubsRepository _personsClubsRepository;
private readonly ISessionService _sessionService;
private readonly IMapper _mapper;
+ private readonly ILogger _logger;
public ClubsService(IClubsRepository clubsRepository, IPersonsClubsRepository personsClubsRepository,
- ISessionService sessionService, IMapper mapper)
+ ISessionService sessionService, IMapper mapper, ILogger logger)
{
_clubsRepository = clubsRepository;
_personsClubsRepository = personsClubsRepository;
_sessionService = sessionService;
_mapper = mapper;
+ _logger = logger;
}
-
+
public async Task ToggleSubscription(int id)
{
if (await _clubsRepository.GetByIdAsync(id) is null) throw new NotFoundException(nameof(ClubModel), id);
@@ -46,6 +49,7 @@ public async Task CreateAsync(ClubDTO clubDTO)
{
var club = _mapper.Map(clubDTO);
await _clubsRepository.CreateAsync(club);
+ _logger.Information("Создан клуб с Id ({ClubId}), принадлежащий пользователю с Id ({OwnerId})", club.Id, club.OwnerId);
}
public async Task UpdateAsync(ClubDTO clubDTO, int clubId)
@@ -54,7 +58,12 @@ public async Task UpdateAsync(ClubDTO clubDTO, int clubId)
if (club is null) throw new NotFoundException(nameof(ClubModel), clubId);
club = _mapper.Map(clubDTO, club);
await _clubsRepository.UpdateAsync(club);
+ _logger.Information("Обновлена информация о клубе с Id ({ClubId})", club.Id);
}
- public async Task DeleteByIdAsync(int id) => await _clubsRepository.DeleteByIdAsync(id);
+ public async Task DeleteByIdAsync(int id)
+ {
+ await _clubsRepository.DeleteByIdAsync(id);
+ _logger.Information("Удалён клуб с Id ({ClubId})", id);
+ }
}
\ No newline at end of file
diff --git a/src/Application/Services/PersonsService.cs b/src/Application/Services/PersonsService.cs
index 7af208f..be6f578 100644
--- a/src/Application/Services/PersonsService.cs
+++ b/src/Application/Services/PersonsService.cs
@@ -1,4 +1,5 @@
using AutoMapper;
+using Serilog;
using Domain.Entities.Person;
using Application.DTO;
using Application.Exceptions;
@@ -12,32 +13,39 @@ public class PersonsService : IPersonsService
private readonly IPersonsRepository _personsRepository;
private readonly ISessionService _sessionService;
private readonly IMapper _mapper;
+ private readonly ILogger _logger;
- public PersonsService(IPersonsRepository personsRepository, ISessionService sessionService, IMapper mapper)
+ public PersonsService(IPersonsRepository personsRepository, ISessionService sessionService, IMapper mapper, ILogger logger)
{
_personsRepository = personsRepository;
_sessionService = sessionService;
_mapper = mapper;
+ _logger = logger;
}
public async Task CreateAsync(PersonDTO personDTO)
{
var person = _mapper.Map(personDTO);
await _personsRepository.CreateAsync(person);
+ _logger.Information("Создан пользователь с Id ({PersonId})", person.Id);
}
public async Task UpdateAsync(PersonDTO personDTO, int id)
{
- if (_sessionService.PersonId != id) throw new UnauthorizedAccessException();
+ if (_sessionService.PersonId != id)
+ throw new ApplicationAuthorizationException("Только владелец записи пользователя может её изменить");
var person = await _personsRepository.GetByIdAsync(id);
if (person is null) throw new NotFoundException(nameof(PersonModel), id);
person = _mapper.Map(personDTO, person);
await _personsRepository.UpdateAsync(person);
+ _logger.Information("Обновлена информация о пользователе с Id ({PersonId})", person.Id);
}
public async Task DeleteByIdAsync(int id)
{
- if (_sessionService.PersonId != id) throw new UnauthorizedAccessException();
+ if (_sessionService.PersonId != id)
+ throw new ApplicationAuthorizationException("Только владелец записи пользователя может её удалить");
await _personsRepository.DeleteByIdAsync(id);
+ _logger.Information("Удалён пользователь с Id ({PersonId})", id);
}
}
\ No newline at end of file
diff --git a/src/Application/Services/PostsService.cs b/src/Application/Services/PostsService.cs
index a44e519..843cfe2 100644
--- a/src/Application/Services/PostsService.cs
+++ b/src/Application/Services/PostsService.cs
@@ -1,4 +1,5 @@
using AutoMapper;
+using Serilog;
using Domain.Entities.Post;
using Application.DTO;
using Application.Exceptions;
@@ -12,21 +13,25 @@ namespace Application.Services;
public class PostsService : IPostsService
{
private readonly IPostsRepository _postsRepository;
+ private readonly IPostsImagesRepository _postsImagesRepository;
private readonly IClubsQueryService _clubsQueryService;
private readonly ISessionService _sessionService;
private readonly IClubAdminsRepository _clubAdminsRepository;
private readonly IClubsRepository _clubsRepository;
private readonly IMapper _mapper;
+ private readonly ILogger _logger;
- public PostsService(IPostsRepository postsRepository, IClubsQueryService clubsQueryService, IMapper mapper,
- IClubAdminsRepository clubAdminsRepository, IClubsRepository clubsRepository, ISessionService sessionService)
+ public PostsService(IPostsRepository postsRepository, IPostsImagesRepository postsImagesRepository, IClubsQueryService clubsQueryService, IMapper mapper,
+ IClubAdminsRepository clubAdminsRepository, IClubsRepository clubsRepository, ISessionService sessionService, ILogger logger)
{
_postsRepository = postsRepository;
+ _postsImagesRepository = postsImagesRepository;
_clubsQueryService = clubsQueryService;
_clubAdminsRepository = clubAdminsRepository;
_clubsRepository = clubsRepository;
_sessionService = sessionService;
_mapper = mapper;
+ _logger = logger;
}
public async Task CreateAsync(PostDTO postDTO)
@@ -35,9 +40,18 @@ public async Task CreateAsync(PostDTO postDTO)
var ownerId = (await _clubsRepository.GetByIdAsync(postDTO.ClubId!.Value))!.OwnerId;
if (ownerId != _sessionService.PersonId &&
!await _clubAdminsRepository.CheckIfContainsAsync(_sessionService.PersonId, postDTO.ClubId!.Value))
- throw new UnauthorizedAccessException("Создавать посты могут только администраторы и владелец клуба");
+ throw new ApplicationAuthorizationException("Создавать посты могут только администраторы и владелец клуба");
var post = _mapper.Map(postDTO);
await _postsRepository.CreateAsync(post);
+ foreach (var imageId in postDTO.PostImagesIds)
+ {
+ await _postsImagesRepository.CreateAsync(new PostsImages
+ {
+ PostId = post.Id,
+ ImageId = imageId
+ });
+ }
+ _logger.Information("Создан пост с Id ({PostId}) пользователем с Id ({PersonId})", post.Id, _sessionService.PersonId);
return post.Id;
}
@@ -48,9 +62,10 @@ public async Task UpdateAsync(PostDTO postDTO, int postId)
var ownerId = (await _clubsRepository.GetByIdAsync(postDTO.ClubId!.Value))!.OwnerId;
if (ownerId != _sessionService.PersonId &&
!await _clubAdminsRepository.CheckIfContainsAsync(_sessionService.PersonId, postDTO.ClubId!.Value))
- throw new UnauthorizedAccessException("Изменять посты могут только администраторы и владелец клуба");
+ throw new ApplicationAuthorizationException("Изменять посты могут только администраторы и владелец клуба");
post = _mapper.Map(postDTO, post);
await _postsRepository.UpdateAsync(post);
+ _logger.Information("Изменён пост с Id ({PostId}) пользователем с Id ({PersonId})", post.Id, _sessionService.PersonId);
}
public async Task DeleteByIdAsync(int postId)
@@ -61,7 +76,8 @@ public async Task DeleteByIdAsync(int postId)
var ownerId = (await _clubsRepository.GetByIdAsync(clubId))!.OwnerId;
if (ownerId != _sessionService.PersonId &&
!await _clubAdminsRepository.CheckIfContainsAsync(_sessionService.PersonId, clubId))
- throw new UnauthorizedAccessException("Удалять посты могут только администраторы и владелец клуба");
+ throw new ApplicationAuthorizationException("Удалять посты могут только администраторы и владелец клуба");
await _postsRepository.DeleteByIdAsync(postId);
+ _logger.Information("Удалён пост с Id ({PostId}) пользователем с Id ({PersonId})", post.Id, _sessionService.PersonId);
}
}
\ No newline at end of file
diff --git a/src/Application/Services/ReportsService.cs b/src/Application/Services/ReportsService.cs
new file mode 100644
index 0000000..0b31490
--- /dev/null
+++ b/src/Application/Services/ReportsService.cs
@@ -0,0 +1,80 @@
+using Domain.Entities.Admin;
+using Domain.Entities.Club;
+using Domain.Entities.Person;
+using Domain.Entities.Post;
+using Application.DTO;
+using Application.Exceptions;
+using Application.Interfaces.Repositories.Admin;
+using Application.Interfaces.Repositories.Club;
+using Application.Interfaces.Repositories.Person;
+using Application.Interfaces.Repositories.Post;
+using Application.Interfaces.Services;
+
+namespace Application.Services;
+
+public class ReportsService : IReportsService
+{
+ private readonly IReportsRepository _reportsRepository;
+ private readonly IReportCategoriesRepository _reportCategoriesRepository;
+ private readonly IReportEntitiesRepository _reportEntitiesRepository;
+ private readonly IPersonsRepository _personsRepository;
+ private readonly IPostsRepository _postsRepository;
+ private readonly IClubsRepository _clubsRepository;
+ private readonly ISessionService _sessionService;
+
+ public ReportsService(IReportsRepository reportsRepository, IReportCategoriesRepository reportCategoriesRepository,
+ IReportEntitiesRepository reportEntitiesRepository, IPersonsRepository personsRepository,
+ IPostsRepository postsRepository, IClubsRepository clubsRepository, ISessionService sessionService)
+ {
+ _reportsRepository = reportsRepository;
+ _reportCategoriesRepository = reportCategoriesRepository;
+ _reportEntitiesRepository = reportEntitiesRepository;
+ _personsRepository = personsRepository;
+ _postsRepository = postsRepository;
+ _clubsRepository = clubsRepository;
+ _sessionService = sessionService;
+ }
+
+ public async Task CreateReport(ReportDTO reportDTO)
+ {
+ if (await _reportEntitiesRepository.GetByIdAsync(reportDTO.TargetEntityId!.Value) is null)
+ throw new NotFoundException(nameof(ReportEntity), reportDTO.TargetEntityId.Value);
+
+ var reportCategory = await _reportCategoriesRepository.GetByIdAsync(reportDTO.CategoryId!.Value);
+ if (reportCategory is null)
+ throw new NotFoundException(nameof(ReportCategory), reportDTO.CategoryId.Value);
+ if (reportCategory.ReportEntityId != reportDTO.TargetEntityId.Value)
+ throw new ArgumentException("Некорректная категория");
+
+ var report = new Report
+ {
+ ReporterId = _sessionService.PersonId,
+ CategoryId = reportDTO.CategoryId!.Value,
+ Description = reportDTO.Description,
+ IsActive = true
+ };
+
+ switch (reportDTO.TargetEntityId.Value)
+ {
+ case (int) ReportTargetEntities.Person:
+ if (await _personsRepository.GetByIdAsync(reportDTO.TargetId!.Value) is null)
+ throw new NotFoundException(nameof(PersonModel), reportDTO.TargetId.Value);
+ report.TargetPersonId = reportDTO.TargetId;
+ break;
+ case (int) ReportTargetEntities.Club:
+ if (await _clubsRepository.GetByIdAsync(reportDTO.TargetId!.Value) is null)
+ throw new NotFoundException(nameof(ClubModel), reportDTO.TargetId.Value);
+ report.TargetClubId = reportDTO.TargetId;
+ break;
+ case (int) ReportTargetEntities.Post:
+ if (await _postsRepository.GetByIdAsync(reportDTO.TargetId!.Value) is null)
+ throw new NotFoundException(nameof(PostModel), reportDTO.TargetId.Value);
+ report.TargetPostId = reportDTO.TargetId;
+ break;
+ default:
+ throw new ArgumentException("Указана некорректная сущность при создании жалобы");
+ }
+
+ await _reportsRepository.CreateAsync(report);
+ }
+}
\ No newline at end of file
diff --git a/src/Domain/Entities/Admin/Report.cs b/src/Domain/Entities/Admin/Report.cs
new file mode 100644
index 0000000..a05c01d
--- /dev/null
+++ b/src/Domain/Entities/Admin/Report.cs
@@ -0,0 +1,13 @@
+namespace Domain.Entities.Admin;
+
+public class Report
+{
+ public int Id { get; set; }
+ public int ReporterId { get; set; }
+ public int? TargetPersonId { get; set; }
+ public int? TargetClubId { get; set; }
+ public int? TargetPostId { get; set; }
+ public int CategoryId { get; set; }
+ public string? Description { get; set; }
+ public bool IsActive { get; set; }
+}
\ No newline at end of file
diff --git a/src/Domain/Entities/Admin/ReportCategory.cs b/src/Domain/Entities/Admin/ReportCategory.cs
new file mode 100644
index 0000000..a4c69fd
--- /dev/null
+++ b/src/Domain/Entities/Admin/ReportCategory.cs
@@ -0,0 +1,8 @@
+namespace Domain.Entities.Admin;
+
+public class ReportCategory
+{
+ public int Id { get; set; }
+ public int ReportEntityId { get; set; }
+ public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/Domain/Entities/Admin/ReportEntity.cs b/src/Domain/Entities/Admin/ReportEntity.cs
new file mode 100644
index 0000000..5ad11e3
--- /dev/null
+++ b/src/Domain/Entities/Admin/ReportEntity.cs
@@ -0,0 +1,7 @@
+namespace Domain.Entities.Admin;
+
+public class ReportEntity
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
index 886a632..87cf9a0 100644
--- a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -1,6 +1,7 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Serilog;
using Application.Interfaces.Repositories;
using Application.Interfaces.Services;
using Application.Interfaces.Queries;
@@ -14,8 +15,11 @@ namespace Infrastructure.Extensions;
public static class ServiceCollectionExtensions
{
+ private static readonly ILogger _logger = Log.Logger;
+
public static IServiceCollection AddStudHubServices(this IServiceCollection services, string connectionString)
{
+ _logger.Debug("Начата регистрация сервисов платформы");
services.AddDbContext(dbContextOptions =>
dbContextOptions.UseNpgsql(connectionString));
services.AddIdentityServices(connectionString);
@@ -38,18 +42,24 @@ public static IServiceCollection AddStudHubServices(this IServiceCollection serv
services.AddTransient();
services.AddScoped();
services.AddStudHubRepositories();
+ _logger.Debug("Завершена регистрация сервисов платформы");
return services;
}
public static IServiceCollection AddAdminServices(this IServiceCollection services)
{
+ _logger.Debug("Начата регистрация сервисов панели администрирования");
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ _logger.Debug("Завершена регистрация сервисов панели администрирования");
return services;
}
public static IServiceCollection AddStudHubRepositories(this IServiceCollection services)
{
+ _logger.Debug("Начата регистрация репозиториев");
foreach (var type in Assembly.GetExecutingAssembly().GetTypes()
.Where(type =>
type is { IsClass: true, IsAbstract: false } && type.GetInterfaces().Contains(typeof(IRepository))))
@@ -62,8 +72,10 @@ public static IServiceCollection AddStudHubRepositories(this IServiceCollection
else
{
services.AddTransient(type);
+ _logger.Warning("Для репозитория {ServiceType} не найден подходящий интерфейс", type);
}
}
+ _logger.Debug("Завершена регистрация репозиториев");
return services;
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/Identity/Services/AuthService.cs b/src/Infrastructure/Identity/Services/AuthService.cs
index dd97014..da73895 100644
--- a/src/Infrastructure/Identity/Services/AuthService.cs
+++ b/src/Infrastructure/Identity/Services/AuthService.cs
@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
+using Serilog;
using Domain.Entities.Person;
using Application.DTO;
using Application.Interfaces.Repositories.Person;
@@ -21,20 +22,24 @@ public class AuthService
private readonly IConfiguration _configuration;
private readonly RefreshTokensRepository _refreshTokensRepository;
private readonly IPersonsRepository _personsRepository;
+ private readonly ILogger _logger;
- public AuthService(UserManager userManager, IConfiguration configuration, RefreshTokensRepository refreshTokensRepository, IPersonsRepository personsRepository)
+ public AuthService(UserManager userManager, IConfiguration configuration,
+ RefreshTokensRepository refreshTokensRepository, IPersonsRepository personsRepository, ILogger logger)
{
_userManager = userManager;
_configuration = configuration;
_refreshTokensRepository = refreshTokensRepository;
_personsRepository = personsRepository;
+ _logger = logger;
}
-
+
public async Task Login(LoginDTO loginDTO)
{
var account = await _userManager.FindByEmailAsync(loginDTO.Email);
if (account is null || !await _userManager.CheckPasswordAsync(account, loginDTO.Password))
throw new InvalidCredentialException("Неверный email или пароль");
+ _logger.Information("Произведён вход в систему пользователем {Email}", account.Email);
return await GetTokensAsync(account);
}
@@ -69,6 +74,8 @@ public async Task Register(RegisterDTO registerDTO)
"и включает в себя как минимум одну цифру, символ нижнего и верхнего регистра, специальный символ, " +
"затем повторите попытку");
}
+ _logger.Information("Произведена регистрация пользователя {Email} с Id ({PersonId})",
+ account.Email, person.Id);
}
public async Task RefreshTokensAsync(TokenModel tokenModel)
@@ -85,9 +92,11 @@ public async Task RefreshTokensAsync(TokenModel tokenModel)
var account = await _userManager.FindByIdAsync(refreshToken.AccountId);
if (account is null) throw new AuthenticationException("Пользователь не найден");
await RevokeRefreshTokenAsync(tokenModel.RefreshToken);
- return await GetTokensAsync(account);
+ tokenModel = await GetTokensAsync(account);
+ _logger.Information("Токены аутентификации обновлены для пользователя с Id ({PersonId})", account.PersonId);
+ return tokenModel;
}
-
+
private async Task GetTokensAsync(Account account)
{
var person = (await _personsRepository.GetByIdAsync(account.PersonId))!;
@@ -117,10 +126,14 @@ private async Task GetTokensAsync(Account account)
ExpiresAt = DateTime.Now.AddMonths(1)
};
await _refreshTokensRepository.AddAsync(refreshToken);
+ _logger.Debug("Создана новая пара токенов аутентификации для пользователя с Id ({PersonId})", person.Id);
TokenModel tokenModel = new TokenModel(accessTokenValue, refreshTokenValue);
return tokenModel;
}
- public async Task RevokeRefreshTokenAsync(string refreshTokenValue) =>
+ public async Task RevokeRefreshTokenAsync(string refreshTokenValue)
+ {
await _refreshTokensRepository.RemoveByValueAsync(refreshTokenValue);
+ _logger.Debug("Аннулирован токен со значением ({RefreshTokenValue})", refreshTokenValue);
+ }
}
\ No newline at end of file
diff --git a/src/Infrastructure/Persistence/Data/ApplicationDbContext.cs b/src/Infrastructure/Persistence/Data/ApplicationDbContext.cs
index ca317a2..4ead819 100644
--- a/src/Infrastructure/Persistence/Data/ApplicationDbContext.cs
+++ b/src/Infrastructure/Persistence/Data/ApplicationDbContext.cs
@@ -35,6 +35,9 @@ public class ApplicationDbContext : DbContext
public DbSet v_PostDetails { get; set; }
public DbSet v_CommentDetails { get; set; }
public DbSet v_ClubCreationRequestDetails { get; set; }
+ public DbSet t_ReportEntities { get; set; }
+ public DbSet t_ReportCategories { get; set; }
+ public DbSet t_Reports { get; set; }
public ApplicationDbContext(DbContextOptions options) : base(options)
{
diff --git a/src/Infrastructure/Persistence/Repositories/Admin/ReportCategoriesRepository.cs b/src/Infrastructure/Persistence/Repositories/Admin/ReportCategoriesRepository.cs
new file mode 100644
index 0000000..0cb2b4f
--- /dev/null
+++ b/src/Infrastructure/Persistence/Repositories/Admin/ReportCategoriesRepository.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore;
+using Domain.Entities.Admin;
+using Application.Interfaces.Repositories.Admin;
+using Infrastructure.Persistence.Data;
+
+namespace Infrastructure.Persistence.Repositories.Admin;
+
+public class ReportCategoriesRepository : IReportCategoriesRepository
+{
+ private readonly ApplicationDbContext _applicationDbContext;
+ private readonly DbSet _reportCategories;
+
+ public ReportCategoriesRepository(ApplicationDbContext applicationDbContext)
+ {
+ _applicationDbContext = applicationDbContext;
+ _reportCategories = _applicationDbContext.Set();
+ }
+
+ public async Task GetByIdAsync(int id) => await _reportCategories.FindAsync(id);
+
+ public async Task CreateAsync(ReportCategory reportCategory)
+ {
+ _reportCategories.Add(reportCategory);
+ await _applicationDbContext.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Persistence/Repositories/Admin/ReportEntitiesRepository.cs b/src/Infrastructure/Persistence/Repositories/Admin/ReportEntitiesRepository.cs
new file mode 100644
index 0000000..51b4f31
--- /dev/null
+++ b/src/Infrastructure/Persistence/Repositories/Admin/ReportEntitiesRepository.cs
@@ -0,0 +1,20 @@
+using Microsoft.EntityFrameworkCore;
+using Domain.Entities.Admin;
+using Application.Interfaces.Repositories.Admin;
+using Infrastructure.Persistence.Data;
+
+namespace Infrastructure.Persistence.Repositories.Admin;
+
+public class ReportEntitiesRepository : IReportEntitiesRepository
+{
+ private readonly DbSet _reportEntities;
+ private readonly ApplicationDbContext _applicationDbContext;
+
+ public ReportEntitiesRepository(ApplicationDbContext applicationDbContext)
+ {
+ _applicationDbContext = applicationDbContext;
+ _reportEntities = _applicationDbContext.Set();
+ }
+
+ public async Task GetByIdAsync(int id) => await _reportEntities.FindAsync(id);
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Persistence/Repositories/Admin/ReportsRepository.cs b/src/Infrastructure/Persistence/Repositories/Admin/ReportsRepository.cs
new file mode 100644
index 0000000..9fb4880
--- /dev/null
+++ b/src/Infrastructure/Persistence/Repositories/Admin/ReportsRepository.cs
@@ -0,0 +1,32 @@
+using Application.Interfaces.Repositories.Admin;
+using Domain.Entities.Admin;
+using Infrastructure.Persistence.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Persistence.Repositories.Admin;
+
+public class ReportsRepository : IReportsRepository
+{
+ private readonly ApplicationDbContext _applicationDbContext;
+ private readonly DbSet _reports;
+
+ public ReportsRepository(ApplicationDbContext applicationDbContext)
+ {
+ _applicationDbContext = applicationDbContext;
+ _reports = _applicationDbContext.Set();
+ }
+
+ public async Task GetByIdAsync(int id) => await _reports.FindAsync(id);
+
+ public async Task CreateAsync(Report report)
+ {
+ _reports.Add(report);
+ await _applicationDbContext.SaveChangesAsync();
+ }
+
+ public async Task UpdateAsync(Report report)
+ {
+ _reports.Update(report);
+ await _applicationDbContext.SaveChangesAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Services/ImagesService.cs b/src/Infrastructure/Services/ImagesService.cs
index 4905603..747e39a 100644
--- a/src/Infrastructure/Services/ImagesService.cs
+++ b/src/Infrastructure/Services/ImagesService.cs
@@ -1,3 +1,4 @@
+using Serilog;
using Domain.Entities;
using Application.Exceptions;
using Application.Interfaces.Repositories;
@@ -8,11 +9,13 @@ namespace Infrastructure.Services;
public class ImagesService : IImagesService
{
private readonly IImagesRepository _imagesRepository;
+ private readonly ILogger _logger;
private readonly string[] _availableImageExtensions = new[] { ".png", ".jpg", ".jpeg" };
- public ImagesService(IImagesRepository imagesRepository)
+ public ImagesService(IImagesRepository imagesRepository, ILogger logger)
{
_imagesRepository = imagesRepository;
+ _logger = logger;
}
public async Task GetPathByIdAsync(int id)
@@ -25,27 +28,38 @@ public async Task GetPathByIdAsync(int id)
public async Task CreateAsync(Stream stream, string extension)
{
if (!_availableImageExtensions.Contains(extension))
+ {
+ _logger.Warning("Загрузка файла с расширением {FileExtension} прервана", extension);
throw new ArgumentException("Расширение файла не поддерживается");
+ }
var image = new Image
{
Path = Guid.NewGuid() + extension,
CreatedAt = DateTime.Now
};
+ _logger.Information("Начата загрузка изображения {Image}", image);
using (var fileStream = File.Create(Path.Combine("/StudHubImages", image.Path)))
{
await stream.CopyToAsync(fileStream);
}
+ _logger.Debug("Изображение {Image} сохранено на диск", image);
await _imagesRepository.CreateAsync(image);
+ _logger.Information("Изображение {Image} успешно загружено", image);
return image.Id;
}
public async Task DeleteImageFileByIdAsync(int id)
{
var filePath = Path.Combine("/StudHubImages", await GetPathByIdAsync(id));
+ _logger.Information("Начато удаление изображения с Id ({ImageId})", id);
await _imagesRepository.DeleteByIdAsync(id);
+ _logger.Debug("Изображение с Id ({ImageId}) успешно удалено из базы данных", id);
if (File.Exists(filePath))
{
File.Delete(filePath);
+ _logger.Information("Изображение с Id ({ImageId}) успешно удалено", id);
+ return;
}
+ _logger.Warning("Не удалось найти изображение по пути ({FilePath}) при удалении", filePath);
}
}
\ No newline at end of file
diff --git a/src/Infrastructure/Services/Queries/ReportsQueryService.cs b/src/Infrastructure/Services/Queries/ReportsQueryService.cs
new file mode 100644
index 0000000..ab0c650
--- /dev/null
+++ b/src/Infrastructure/Services/Queries/ReportsQueryService.cs
@@ -0,0 +1,46 @@
+using Application;
+using Microsoft.EntityFrameworkCore;
+using Domain.Entities.Admin;
+using Application.Exceptions;
+using Application.Interfaces.Queries;
+using Infrastructure.Persistence.Data;
+
+namespace Infrastructure.Services.Queries;
+
+public class ReportsQueryService : IReportsQueryService
+{
+ private readonly DbSet _reports;
+
+ public ReportsQueryService(ApplicationDbContext context)
+ {
+ _reports = context.t_Reports;
+ }
+
+ public async Task> GetAllAsync() => await _reports.ToListAsync();
+
+ public async Task GetByIdAsync(int id)
+ {
+ var report = await _reports.FindAsync(id);
+ if (report is null)
+ throw new NotFoundException(nameof(Report), id);
+ return report;
+ }
+
+ public async Task> GetByTargetEntityIdAsync(ReportTargetEntities entity)
+ {
+ ICollection reports = new List();
+ switch (entity)
+ {
+ case ReportTargetEntities.Person:
+ reports = await _reports.Where(r => r.TargetPersonId != null).ToListAsync();
+ break;
+ case ReportTargetEntities.Club:
+ reports = await _reports.Where(r => r.TargetClubId != null).ToListAsync();
+ break;
+ case ReportTargetEntities.Post:
+ reports = await _reports.Where(r => r.TargetPostId != null).ToListAsync();
+ break;
+ }
+ return reports;
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Services/SessionService.cs b/src/Infrastructure/Services/SessionService.cs
index a706972..6b61264 100644
--- a/src/Infrastructure/Services/SessionService.cs
+++ b/src/Infrastructure/Services/SessionService.cs
@@ -1,12 +1,20 @@
using System.Security.Authentication;
using System.Security.Claims;
+using Serilog;
using Application.Interfaces.Services;
namespace Infrastructure.Services;
public class SessionService : ISessionService
{
+ private readonly ILogger _logger;
private int? _personId;
+
+ public SessionService(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public int PersonId
{
get => _personId ?? throw new AuthenticationException("Сессия не инициализирована");
@@ -17,5 +25,6 @@ public void Initialise(ClaimsPrincipal claimsPrincipal)
{
PersonId = int.Parse(claimsPrincipal.FindFirstValue("personId")
?? throw new AuthenticationException("При инициализации использован недействительный ClaimsPrincipal"));
+ _logger.Debug("Инициализирована сессия для пользователя с Id ({PersonId})", PersonId);
}
}
\ No newline at end of file
diff --git a/src/Platform.API/Controllers/PostsController.cs b/src/Platform.API/Controllers/PostsController.cs
index 0721050..a982e61 100644
--- a/src/Platform.API/Controllers/PostsController.cs
+++ b/src/Platform.API/Controllers/PostsController.cs
@@ -3,6 +3,7 @@
using Application.DTO;
using Application.Interfaces.Queries;
using Application.Interfaces.Services;
+using Platform.API.Models;
namespace Platform.API.Controllers
{
@@ -12,11 +13,13 @@ public class PostsController : ControllerBase
{
private readonly IPostsQueryService _postsQueryService;
private readonly IPostsService _postsService;
+ private readonly IImagesService _imagesService;
- public PostsController(IPostsQueryService postsQueryService, IPostsService postsService)
+ public PostsController(IPostsQueryService postsQueryService, IPostsService postsService, IImagesService imagesService)
{
_postsQueryService = postsQueryService;
_postsService = postsService;
+ _imagesService = imagesService;
}
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -45,9 +48,20 @@ public async Task> GetById(int id)
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[HttpPost]
- public async Task> Create(PostDTO post)
+ public async Task> Create(CreatePostRequest request)
{
- var id = await _postsService.CreateAsync(post);
+ var postDTO = new PostDTO
+ {
+ Title = request.Title,
+ Content = request.Content,
+ ClubId = request.ClubId
+ };
+ foreach (var image in request.PostImages)
+ {
+ var imageExtension = Path.GetExtension(image.FileName);
+ postDTO.PostImagesIds.Add(await _imagesService.CreateAsync(image.OpenReadStream(), imageExtension));
+ }
+ var id = await _postsService.CreateAsync(postDTO);
return CreatedAtAction(nameof(GetById), new { id }, await _postsQueryService.GetDetailByIdAsync(id));
}
diff --git a/src/Platform.API/ExceptionHandlers/AuthorizationExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
similarity index 62%
rename from src/Platform.API/ExceptionHandlers/AuthorizationExceptionHandler.cs
rename to src/Platform.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
index 2fc4ab2..d1a81c3 100644
--- a/src/Platform.API/ExceptionHandlers/AuthorizationExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/ApplicationAuthorizationExceptionHandler.cs
@@ -1,16 +1,25 @@
+using Application.Exceptions;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
-public class AuthorizationExceptionHandler : IExceptionHandler
+public class ApplicationAuthorizationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Доступ запрещён";
private const int StatusCode = StatusCodes.Status403Forbidden;
+ private readonly ILogger _logger;
+
+ public ApplicationAuthorizationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
- if (exception is not UnauthorizedAccessException)
+ if (exception is not ApplicationAuthorizationException)
{
return false;
}
@@ -23,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/ExceptionHandlers/AuthenticationExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
index ac44437..a6c552b 100644
--- a/src/Platform.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/AuthenticationExceptionHandler.cs
@@ -1,6 +1,7 @@
using System.Security.Authentication;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class AuthenticationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "При аутентификации произошла ошибка";
private const int StatusCode = StatusCodes.Status401Unauthorized;
+ private readonly ILogger _logger;
+
+ public AuthenticationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/ExceptionHandlers/BadRequestExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/BadRequestExceptionHandler.cs
index ce7561d..c12e946 100644
--- a/src/Platform.API/ExceptionHandlers/BadRequestExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/BadRequestExceptionHandler.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
@@ -7,6 +8,13 @@ public class BadRequestExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Некорректный запрос";
private const int StatusCode = StatusCodes.Status400BadRequest;
+ private readonly ILogger _logger;
+
+ public BadRequestExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -23,6 +31,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/ExceptionHandlers/GlobalExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/GlobalExceptionHandler.cs
index 7692133..fa14194 100644
--- a/src/Platform.API/ExceptionHandlers/GlobalExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/GlobalExceptionHandler.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
@@ -7,6 +8,13 @@ public class GlobalExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Произошла ошибка на стороне сервера";
private const int StatusCode = StatusCodes.Status500InternalServerError;
+ private readonly ILogger _logger;
+
+ public GlobalExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -19,6 +27,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Error(exception, ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
index 76a6547..91e569c 100644
--- a/src/Platform.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/InvalidOperationExceptionHandler.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class InvalidOperationExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Недопустимая операция";
private const int StatusCode = StatusCodes.Status409Conflict;
+ private readonly ILogger _logger;
+
+ public InvalidOperationExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/ExceptionHandlers/NotFoundExceptionHandler.cs b/src/Platform.API/ExceptionHandlers/NotFoundExceptionHandler.cs
index d284aee..7c5bc7d 100644
--- a/src/Platform.API/ExceptionHandlers/NotFoundExceptionHandler.cs
+++ b/src/Platform.API/ExceptionHandlers/NotFoundExceptionHandler.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Application.Exceptions;
+using ILogger = Serilog.ILogger;
namespace Platform.API.ExceptionHandlers;
@@ -8,6 +9,13 @@ public class NotFoundExceptionHandler : IExceptionHandler
{
private const string ResponseTitle = "Запрашиваемый ресурс отсутствует на сервере";
private const int StatusCode = StatusCodes.Status404NotFound;
+ private readonly ILogger _logger;
+
+ public NotFoundExceptionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception,
CancellationToken cancellationToken)
{
@@ -24,6 +32,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ _logger.Warning(exception, "Исключение обработано: {ResponseTitle}", ResponseTitle);
return true;
}
}
\ No newline at end of file
diff --git a/src/Platform.API/Extensions/ResponseCookiesExtensions.cs b/src/Platform.API/Extensions/ResponseCookiesExtensions.cs
index 3c729f2..ae0e229 100644
--- a/src/Platform.API/Extensions/ResponseCookiesExtensions.cs
+++ b/src/Platform.API/Extensions/ResponseCookiesExtensions.cs
@@ -1,9 +1,13 @@
+using Serilog;
+using ILogger = Serilog.ILogger;
using Infrastructure.Identity.Models.DTO;
namespace Platform.API.Extensions;
public static class ResponseCookiesExtensions
{
+ private static readonly ILogger _logger = Log.Logger;
+
public static void UpdateTokenCookies(this IResponseCookies cookies, TokenModel tokens)
{
cookies.DeleteTokenCookies();
@@ -14,11 +18,13 @@ public static void UpdateTokenCookies(this IResponseCookies cookies, TokenModel
};
cookies.Append("AccessToken", tokens.AccessToken, cookieOptions);
cookies.Append("RefreshToken", tokens.RefreshToken, cookieOptions);
+ _logger.Debug("Обновлены Cookie файлы Access и Refresh токенов");
}
public static void DeleteTokenCookies(this IResponseCookies cookies)
{
cookies.Delete("AccessToken");
cookies.Delete("RefreshToken");
+ _logger.Debug("Удалены Cookie файлы Access и Refresh токенов");
}
}
\ No newline at end of file
diff --git a/src/Platform.API/Extensions/ServiceCollectionExtensions.cs b/src/Platform.API/Extensions/ServiceCollectionExtensions.cs
index 459d0b0..9fa18c9 100644
--- a/src/Platform.API/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Platform.API/Extensions/ServiceCollectionExtensions.cs
@@ -10,7 +10,7 @@ public static IServiceCollection AddExceptionHandlers(this IServiceCollection se
services.AddExceptionHandler();
services.AddExceptionHandler();
services.AddExceptionHandler();
- services.AddExceptionHandler();
+ services.AddExceptionHandler();
services.AddExceptionHandler();
return services;
}
diff --git a/src/Platform.API/Models/CreatePostRequest.cs b/src/Platform.API/Models/CreatePostRequest.cs
new file mode 100644
index 0000000..281a6f8
--- /dev/null
+++ b/src/Platform.API/Models/CreatePostRequest.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Platform.API.Models;
+
+public record CreatePostRequest
+{
+ [Required]
+ [MaxLength(50)]
+ public required string Title { get; init; }
+ [Required]
+ [MaxLength(500)]
+ public required string Content { get; init; }
+ [Required]
+ public int? ClubId { get; init; }
+ [MaxLength(5)]
+ public List PostImages { get; init; } = new List();
+}
\ No newline at end of file
diff --git a/src/Platform.API/Platform.API.csproj b/src/Platform.API/Platform.API.csproj
index ed3f567..89d0fc0 100644
--- a/src/Platform.API/Platform.API.csproj
+++ b/src/Platform.API/Platform.API.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Platform.API/Program.cs b/src/Platform.API/Program.cs
index d122b3a..8bb1bb2 100644
--- a/src/Platform.API/Program.cs
+++ b/src/Platform.API/Program.cs
@@ -1,8 +1,11 @@
+using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
using Scalar.AspNetCore;
+using Serilog;
+using Serilog.Filters;
using Application.Interfaces.Services;
using Infrastructure.Extensions;
using Platform.API.Extensions;
@@ -14,6 +17,13 @@ public class Program
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
+ builder.Host.UseSerilog((context, configuration) =>
+ {
+ configuration.ReadFrom.Configuration(context.Configuration);
+ // Временное решение, дублирование логов при использовании ExceptionHandlerMiddleware исправлено в .NET 10
+ configuration.Filter.ByExcluding(Matching
+ .FromSource());
+ });
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(options =>
{
@@ -81,6 +91,15 @@ public static void Main(string[] args)
options.Secure = CookieSecurePolicy.Always;
});
var app = builder.Build();
+ app.UseSerilogRequestLogging(options =>
+ {
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ var personId = httpContext.User.FindFirstValue("personId") ?? "Undefined";
+ diagnosticContext.Set("PersonId", personId);
+ };
+ options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms for PersonId {PersonId}";
+ });
app.UseCors();
app.UseExceptionHandler();
app.UseOpenApi(options =>
@@ -92,7 +111,9 @@ public static void Main(string[] args)
options.Title = app.Environment.IsDevelopment() ? "StudHub API [DEV]" : "StudHub API";
options.Servers = new List
{
- new ScalarServer("https://dev-api.setka-rtu.ru")
+ new ScalarServer(app.Environment.IsDevelopment()
+ ? "https://dev-api.setka-rtu.ru"
+ : "https://api.setka-rtu.ru")
};
if (app.Environment.IsDevelopment())
{
diff --git a/src/Platform.API/appsettings.Development.json b/src/Platform.API/appsettings.Development.json
index 0c208ae..9f81c7e 100644
--- a/src/Platform.API/appsettings.Development.json
+++ b/src/Platform.API/appsettings.Development.json
@@ -1,8 +1,22 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft.EntityFrameworkCore": "Information",
+ "Microsoft.AspNetCore": "Information"
+ }
+ },
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "/Logs/Platform.API/Development/DevLog-.txt",
+ "rollingInterval": "Day"
+ }
+ }
+ ]
}
}
diff --git a/src/Platform.API/appsettings.json b/src/Platform.API/appsettings.json
index 969e6ca..876b1c3 100644
--- a/src/Platform.API/appsettings.json
+++ b/src/Platform.API/appsettings.json
@@ -1,14 +1,24 @@
{
- "Logging": {
- "LogLevel": {
+ "Serilog": {
+ "MinimumLevel": {
"Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+ "Override": {
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "/Logs/Platform.API/Production/Log-.txt",
+ "rollingInterval": "Day"
+ }
+ }
+ ]
},
"Domain": ".setka-rtu.ru",
- "AllowedHosts": "*",
- "JWTKey": "9>I4=o8{AG\\Nh{`I7m*={tP:Gs)dsXxNw.Hj92]dQSO",
- "ConnectionStrings": {
- "Default": "Host=31.128.38.159;Port=5432;User ID=admin-setka;Password=P1{Hg?:(~jLfDNrp!W9QkF?bO;Database=SetkaDB"
- }
+ "AllowedHosts": "*"
}
\ No newline at end of file
diff --git a/test/Unit/ClubCreationRequestsServiceTests.cs b/test/Unit/ClubCreationRequestsServiceTests.cs
new file mode 100644
index 0000000..fc6e0ae
--- /dev/null
+++ b/test/Unit/ClubCreationRequestsServiceTests.cs
@@ -0,0 +1,257 @@
+using System.ComponentModel.DataAnnotations;
+using AutoMapper;
+using Moq;
+using Serilog;
+using Domain.Entities.Admin;
+using Domain.Entities.Club;
+using Application.DTO;
+using Application.Exceptions;
+using Application.Interfaces.Repositories.Admin;
+using Application.Interfaces.Repositories.Club;
+using Application.Services;
+
+namespace Unit;
+
+public class ClubCreationRequestsServiceTests
+{
+ private readonly Mock _requestsRepoMock;
+ private readonly Mock _clubsRepoMock;
+ private readonly Mock _loggerMock;
+ private readonly Mock _mapperMock;
+ private readonly ClubCreationRequestsService _clubCreationRequestsService;
+ public ClubCreationRequestsServiceTests()
+ {
+ _requestsRepoMock = new Mock();
+ _clubsRepoMock = new Mock();
+ _loggerMock = new Mock();
+ _mapperMock = new Mock();
+ _clubCreationRequestsService = new ClubCreationRequestsService(_requestsRepoMock.Object, _clubsRepoMock.Object,
+ _mapperMock.Object, _loggerMock.Object);
+ }
+ #region CreateRequest
+ [Fact]
+ public void CreateRequest_WithValidData_Succeeds()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = "Новый клуб для тестирования функциональности",
+ PersonId = 0,
+ ImageId = 0,
+ BannerId = 0,
+ ClubAbout = "Описание клуба для создания"
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.True(isValid);
+ }
+
+ [Fact]
+ public void CreateRequest_WithEmptyClubName_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ Comment = "Новый клуб для тестирования функциональности",
+ PersonId = 0,
+ ImageId = 0,
+ BannerId = 0,
+ ClubAbout = new string('0', 100)
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("ClubName") && r.ErrorMessage!.Contains("required"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithLongClubName_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = new string('0', 64),
+ Comment = "Новый клуб для тестирования функциональности",
+ PersonId = 0,
+ ImageId = 0,
+ BannerId = 0,
+ ClubAbout = new string('0', 100)
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("ClubName") && r.ErrorMessage!.Contains("maximum length"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithEmptyImageId_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = "Новый клуб для тестирования функциональности",
+ PersonId = 0
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("ImageId") && r.ErrorMessage!.Contains("required"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithEmptyPersonId_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = "Новый клуб для тестирования функциональности",
+ ImageId = 0
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("PersonId") && r.ErrorMessage!.Contains("required"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithLongClubAbout_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = "Новый клуб для тестирования функциональности",
+ PersonId = 0,
+ ImageId = 0,
+ BannerId = 0,
+ ClubAbout = new string('0', 256)
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("ClubAbout") && r.ErrorMessage!.Contains("maximum length"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithEmptyComment_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = "",
+ PersonId = 0
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("Comment") && r.ErrorMessage!.Contains("required"));
+ }
+
+ [Fact]
+ public void CreateRequest_WithLongComment_ValidationFails()
+ {
+ var clubCreationRequest = new ClubCreationRequestDTO
+ {
+ ClubName = "Новый клуб",
+ Comment = new string('0', 1001),
+ PersonId = 0
+ };
+ var context = new ValidationContext(clubCreationRequest);
+ var results = new List();
+
+ var isValid = Validator.TryValidateObject(clubCreationRequest, context, results, true);
+
+ Assert.False(isValid);
+ Assert.Contains(results, r => r.MemberNames.Contains("Comment") && r.ErrorMessage!.Contains("maximum length"));
+ }
+ [Fact]
+ public async Task AcceptRequest_WhenRequestNotFound_ThrowsNotFoundException()
+ {
+ var requestId = 1;
+ _requestsRepoMock
+ .Setup(r => r.GetByIdAsync(requestId))
+ .ReturnsAsync((ClubCreationRequest)null);
+
+ await Assert.ThrowsAsync(() => _clubCreationRequestsService.AcceptRequest(requestId));
+ }
+
+ [Fact]
+ public async Task AcceptRequest_WhenRequestIsNotActive_ThrowsInvalidOperationException()
+ {
+ var requestId = 1;
+ var inactiveRequest = new ClubCreationRequest { IsActive = false };
+ _requestsRepoMock
+ .Setup(r => r.GetByIdAsync(requestId))
+ .ReturnsAsync(inactiveRequest);
+
+ var exception = await Assert.ThrowsAsync(
+ () => _clubCreationRequestsService.AcceptRequest(requestId));
+ Assert.Equal("Заявка на создание клуба уже была рассмотрена", exception.Message);
+ }
+
+ [Fact]
+ public async Task AcceptRequest_WhenValidRequest_CreatesClubAndUpdatesRequest()
+ {
+ var requestId = 1;
+ var activeRequest = new ClubCreationRequest
+ {
+ Id = requestId,
+ ClubName = "Test Club",
+ ClubAbout = "About club",
+ ImageId = 1,
+ BannerId = 2,
+ PersonId = 3,
+ IsActive = true
+ };
+
+ _requestsRepoMock
+ .Setup(r => r.GetByIdAsync(requestId))
+ .ReturnsAsync(activeRequest);
+
+ ClubModel createdClub = null;
+ _clubsRepoMock
+ .Setup(r => r.CreateAsync(It.IsAny()))
+ .Callback(club => createdClub = club)
+ .Returns(Task.CompletedTask);
+
+ _requestsRepoMock
+ .Setup(r => r.UpdateAsync(activeRequest))
+ .Returns(Task.CompletedTask);
+
+ await _clubCreationRequestsService.AcceptRequest(requestId);
+
+ _clubsRepoMock.Verify(r => r.CreateAsync(It.IsAny()), Times.Once);
+ _requestsRepoMock.Verify(r => r.UpdateAsync(activeRequest), Times.Once);
+
+ Assert.NotNull(createdClub);
+ Assert.Equal(activeRequest.ClubName, createdClub.Name);
+ Assert.Equal(activeRequest.ClubAbout, createdClub.About);
+ Assert.Equal(activeRequest.ImageId, createdClub.ImageId);
+ Assert.Equal(activeRequest.BannerId, createdClub.BannerId);
+ Assert.Equal(activeRequest.PersonId, createdClub.OwnerId);
+ Assert.False(activeRequest.IsActive);
+
+ _loggerMock.Verify(l => l.Information(
+ "Заявка на создание клуба с Id ({RequestId}) одобрена", requestId),
+ Times.Once);
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/test/Unit/Unit.csproj b/test/Unit/Unit.csproj
new file mode 100644
index 0000000..3285fca
--- /dev/null
+++ b/test/Unit/Unit.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+