From 3c785d5c89cb510aff66f452e1795ad8e4a261e6 Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:20:45 +0700 Subject: [PATCH] Switch to SmartRes Result/Error, upgrade to .NET 10 Migrate codebase to use SmartRes Result (and Unit) across services and interfaces, replacing previous Result usage and updating many method signatures and implementations. Add ResultExtensions to convert SmartRes results to ASP.NET ActionResult and refactor AuthController to use functional Bind/Tap chains for registration/login flows. Upgrade projects to .NET 10 and bump related NuGet packages; add libs/SmartRes.dll and project references. Simplify DbContext registration to use Npgsql only with retry policies and enable detailed logging. Update Swagger/launch settings and other minor fixes (AutoMapper registration change, whitespace/exception handling, and removal of the Govor.ConsoleClient files). --- .../ConfigurationProgramExtensions.cs | 62 ++++------- .../Common/Extensions/ResultExtensions.cs | 60 +++++++++++ .../Authentication/AuthController.cs | 98 ++++++------------ .../Friends/FriendshipController.cs | 2 +- Govor.API/Govor.API.csproj | 30 +++--- Govor.API/Program.cs | 34 +++--- Govor.API/Properties/launchSettings.json | 2 +- .../Authentication/AuthService.cs | 15 +-- .../Authentication/IAuthService.cs | 5 +- .../Authentication/IInvitesService.cs | 3 +- .../Authentication/InvitesService.cs | 11 +- .../Friends/FriendRequestCommandService.cs | 23 ++-- .../Friends/IFriendRequestCommandService.cs | 7 +- Govor.Application/Govor.Application.csproj | 28 +++-- Govor.Application/Groups/IGroupService.cs | 7 +- .../AdminsStuff/IInvitationGetter.cs | 3 +- .../AdminsStuff/InvitationGetter.cs | 5 +- .../Validators/IUsernameValidator.cs | 3 +- .../Validators/UsernameValidator.cs | 33 +++--- .../IUserPrivateChatsGetterService.cs | 3 +- .../UserPrivateChatsGetter.cs | 9 +- Govor.Application/Profiles/IProfileService.cs | 7 +- Govor.Application/Profiles/ProfileService.cs | 11 +- .../IPushNotificationService.cs | 7 +- .../PushNotifications/IPushTokenService.cs | 15 +-- .../PushNotificationService.cs | 11 +- .../PushNotifications/PushTokenService.cs | 23 ++-- .../Users/UserSessions/IUserSessionOpener.cs | 3 +- .../Users/UserSessions/IUserSessionReader.cs | 3 +- .../UserSessions/IUserSessionRefresher.cs | 3 +- .../Users/UserSessions/IUserSessionRevoker.cs | 5 +- .../Users/UserSessions/UserSessionOpener.cs | 9 +- .../Users/UserSessions/UserSessionReader.cs | 12 ++- .../UserSessions/UserSessionRefresher.cs | 11 +- .../Users/UserSessions/UserSessionRevoker.cs | 15 ++- .../Govor.ConsoleClient.Tests.csproj | 4 - Govor.ConsoleClient/App.cs | 28 ----- Govor.ConsoleClient/Commands/HelpCommand.cs | 56 ---------- Govor.ConsoleClient/Commands/ICommand.cs | 17 --- .../Commands/IInteractiveCommand.cs | 7 -- .../Commands/SendMessageCommand.cs | 44 -------- Govor.ConsoleClient/DependencyInjection.cs | 46 -------- .../Govor.ConsoleClient.csproj | 29 ------ Govor.ConsoleClient/Program.cs | 18 ---- .../Services/CommandContext.cs | 17 --- .../Extensions/ServiceCollectionExtensions.cs | 18 ---- .../Implementations/CommandDispatcher.cs | 44 -------- .../Services/Implementations/ConsoleLogger.cs | 47 --------- .../Services/Implementations/InputPipeline.cs | 50 --------- .../Implementations/MiddlewarePipeline.cs | 30 ------ .../Services/Interfaces/ICommandDispatcher.cs | 9 -- .../Services/Interfaces/IInputPipeline.cs | 6 -- .../Services/Interfaces/ILogger.cs | 10 -- .../Interfaces/IMiddlewarePipeline.cs | 6 -- .../Middleware/ExceptionHandlingMiddleware.cs | 25 ----- .../Services/Middleware/ICommandMiddleware.cs | 6 -- Govor.ConsoleClient/appsettings.json | 3 - Govor.Contracts/Govor.Contracts.csproj | 4 +- Govor.Domain/Common/Error.cs | 15 ++- Govor.Domain/Common/ErrorType.cs | 11 ++ Govor.Domain/Common/Result.cs | 57 +++------- Govor.Domain/Govor.Domain.csproj | 14 ++- Govor.sln | 13 ++- global.json | 2 +- libs/SmartRes.dll | Bin 0 -> 10752 bytes 65 files changed, 376 insertions(+), 838 deletions(-) create mode 100644 Govor.API/Common/Extensions/ResultExtensions.cs delete mode 100644 Govor.ConsoleClient/App.cs delete mode 100644 Govor.ConsoleClient/Commands/HelpCommand.cs delete mode 100644 Govor.ConsoleClient/Commands/ICommand.cs delete mode 100644 Govor.ConsoleClient/Commands/IInteractiveCommand.cs delete mode 100644 Govor.ConsoleClient/Commands/SendMessageCommand.cs delete mode 100644 Govor.ConsoleClient/DependencyInjection.cs delete mode 100644 Govor.ConsoleClient/Govor.ConsoleClient.csproj delete mode 100644 Govor.ConsoleClient/Program.cs delete mode 100644 Govor.ConsoleClient/Services/CommandContext.cs delete mode 100644 Govor.ConsoleClient/Services/Extensions/ServiceCollectionExtensions.cs delete mode 100644 Govor.ConsoleClient/Services/Implementations/CommandDispatcher.cs delete mode 100644 Govor.ConsoleClient/Services/Implementations/ConsoleLogger.cs delete mode 100644 Govor.ConsoleClient/Services/Implementations/InputPipeline.cs delete mode 100644 Govor.ConsoleClient/Services/Implementations/MiddlewarePipeline.cs delete mode 100644 Govor.ConsoleClient/Services/Interfaces/ICommandDispatcher.cs delete mode 100644 Govor.ConsoleClient/Services/Interfaces/IInputPipeline.cs delete mode 100644 Govor.ConsoleClient/Services/Interfaces/ILogger.cs delete mode 100644 Govor.ConsoleClient/Services/Interfaces/IMiddlewarePipeline.cs delete mode 100644 Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs delete mode 100644 Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs delete mode 100644 Govor.ConsoleClient/appsettings.json create mode 100644 Govor.Domain/Common/ErrorType.cs create mode 100644 libs/SmartRes.dll diff --git a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs index f79650d..2a26bbf 100644 --- a/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs @@ -98,7 +98,7 @@ public static class ConfigurationProgramExtensions // Auto Mapper - services.AddAutoMapper(typeof(MappingProfile)); + services.AddAutoMapper(op => { }, typeof(MappingProfile)); services.AddScoped(); @@ -111,52 +111,26 @@ public static class ConfigurationProgramExtensions services.AddScoped(); } - + public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration) { - var useMySql = configuration.GetValue("UseMySql"); - - if (useMySql) + services.AddDbContext(options => { - services.AddDbContext(options => - { - var connectionString = configuration.GetConnectionString(nameof(GovorDbContext)); + options.UseNpgsql( + configuration.GetConnectionString(nameof(GovorDbContext)), + npgsqlOptions => + { + // retry for transient failures + npgsqlOptions.EnableRetryOnFailure( + 5, + TimeSpan.FromSeconds(5), + null); + }); - options.UseMySql( - connectionString, - new MySqlServerVersion(new Version(8, 0, 21)), - mySqlOptions => - { - mySqlOptions.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorNumbersToAdd: null); - }); - - options.EnableSensitiveDataLogging(); - options.EnableDetailedErrors(); - }); - } - else - { - services.AddDbContext(options => - { - options.UseNpgsql( - configuration.GetConnectionString(nameof(GovorDbContext)), - npgsqlOptions => - { - // retry for transient failures - npgsqlOptions.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(5), - errorCodesToAdd: null); - }); - - //options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - - options.EnableSensitiveDataLogging(); - options.EnableDetailedErrors(); - }); - } + //options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + + options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); + }); } } \ No newline at end of file diff --git a/Govor.API/Common/Extensions/ResultExtensions.cs b/Govor.API/Common/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..aa5cac4 --- /dev/null +++ b/Govor.API/Common/Extensions/ResultExtensions.cs @@ -0,0 +1,60 @@ +using Govor.Domain.Common; +using Microsoft.AspNetCore.Mvc; +using SmartRes; + +namespace Govor.API.Common.Extensions; + +public static class ResultExtensions +{ + public static ActionResult ToActionResult(this Result result) + { + if (result.IsSuccess) + { + // Если тип Unit, возвращаем 204 No Content, иначе 200 OK со значением + return typeof(T) == typeof(Unit) + ? new StatusCodeResult(StatusCodes.Status204NoContent) + : new OkObjectResult(result.Value); + } + + return GenerateProblemDetails(result.Error); + } + + private static ActionResult GenerateProblemDetails(Error error) + { + var statusCode = error.Type switch + { + ErrorType.NotFound => StatusCodes.Status404NotFound, + ErrorType.Validation => StatusCodes.Status400BadRequest, + ErrorType.Conflict => StatusCodes.Status409Conflict, + ErrorType.Unauthorized => StatusCodes.Status401Unauthorized, + ErrorType.Forbidden => StatusCodes.Status403Forbidden, + _ => StatusCodes.Status400BadRequest + }; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = GetTitleForErrorType(error.Type), + Detail = error.Message, + }; + + problemDetails.Extensions.Add("errorCode", error.Code); + + if (error.Errors is not null) + { + problemDetails.Extensions.Add("errors", error.Errors); + } + + return new ObjectResult(problemDetails) { StatusCode = statusCode }; + } + + private static string GetTitleForErrorType(ErrorType type) => type switch + { + ErrorType.NotFound => "Not Found", + ErrorType.Validation => "Validation Error", + ErrorType.Conflict => "Conflict", + ErrorType.Unauthorized => "Unauthorized", + ErrorType.Forbidden => "Forbidden", + _ => "Bad Request" + }; +} \ No newline at end of file diff --git a/Govor.API/Controllers/Authentication/AuthController.cs b/Govor.API/Controllers/Authentication/AuthController.cs index f1c7fa8..11682f7 100644 --- a/Govor.API/Controllers/Authentication/AuthController.cs +++ b/Govor.API/Controllers/Authentication/AuthController.cs @@ -1,9 +1,10 @@ +using Govor.API.Common.Extensions; using Govor.Application.Authentication; -using Govor.Application.Authentication.Exceptions; using Govor.Application.Users.UserSessions; using Govor.Contracts.Requests; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using SmartRes; namespace Govor.API.Controllers.Authentication; @@ -30,82 +31,47 @@ public class AuthController : Controller } [HttpPost("register")] // api/auth/register - public async Task Register([FromBody] RegistrationRequest registrationRequest) + public async Task Register([FromBody] RegistrationRequest request) { - - var inviteResult = await _invitesService.ValidateAsync(registrationRequest.InviteLink); - if (!inviteResult.IsSuccess) + _logger.LogInformation("Processing registration request for: {Name}", request.Name); + + var result = await _invitesService.ValidateAsync(request.InviteLink) + .BindAsync(invite => _accountService.RegistrationAsync(request.Name, request.Password, invite)) + .TapAsync(user => _logger.LogInformation("User {Username} ({Id}) registered successfully", user.Username, user.Id)) + .BindAsync(user => _userSession.OpenSessionAsync(user, request.DeviceInfo)); + + if (result.IsFailure) { - _logger.LogWarning("Invite link invalid: {InviteLink}. Error: {Error}", registrationRequest.InviteLink, - inviteResult.Error); - return BadRequest($"Invite link invalid: {inviteResult.Error.Message}"); + _logger.LogWarning("Registration pipeline failed. Error: {Code} - {Message}", + result.Error.Code, result.Error.Message); + } + else + { + _logger.LogInformation("Session opened successfully for the request."); } - var userResult = await _accountService.RegistrationAsync( - registrationRequest.Name, - registrationRequest.Password, - inviteResult.Value); - - if (userResult.IsFailure) - { - _logger.LogWarning("Registration failed for user {Name}. Error: {Error}", registrationRequest.Name, - userResult.Error); - - return userResult.Error.Code switch - { - nameof(UserAlreadyExistException) => BadRequest($"Registration failed: {userResult.Error.Message}"), - nameof(InvalidUsernameException) => BadRequest($"Invalid username: {userResult.Error.Message}"), - _ => BadRequest($"Registration failed: {userResult.Error.Message}") - }; - } - - var user = userResult.Value; - _logger.LogInformation("Register request for {Username} with id {Id} processed successfully", user.Username, - user.Id); - - var sessionResult = await _userSession.OpenSessionAsync(user, registrationRequest.DeviceInfo); - if (sessionResult.IsFailure) - { - _logger.LogError("Failed to open session for user {Username}. Error: {Error}", user.Username, - sessionResult.Error.Message); - return StatusCode(500, "An error occurred while creating the session."); - } - - _logger.LogInformation("Session for user {Username} with id {Id} has been opened", user.Username, user.Id); - return Ok(sessionResult.Value); + return result.ToActionResult(); } - [HttpPost("login")] // api/auth/login - public async Task Login([FromBody] LoginRequest loginRequest) + public async Task Login([FromBody] LoginRequest request) { - var userResult = await _accountService.LoginAsync(loginRequest.Name, loginRequest.Password); + _logger.LogInformation("Processing registration request for: {Name}", request.Name); + + var result = await _accountService.LoginAsync(request.Name, request.Password) + .TapAsync(user => _logger.LogInformation("User {Username} ({Id}) logged in.", user.Username, user.Id)) + .BindAsync(user => _userSession.OpenSessionAsync(user, request.DeviceInfo)); - if (userResult.IsFailure) + if (result.IsFailure) { - _logger.LogWarning("Login failed for user {Name}. Error: {Code}", loginRequest.Name, userResult.Error); - - return userResult.Error.Code switch - { - nameof(UserNotRegisteredException) => BadRequest("Login failed: user does not exist."), - nameof(InvalidOperationException) => BadRequest("Login failed: username or password is incorrect."), - _ => BadRequest($"Login failed: {userResult.Error.Message}") - }; + _logger.LogWarning("Login pipeline failed. Error: {Code} - {Message}", + result.Error.Code, result.Error.Message); + } + else + { + _logger.LogInformation("Session opened successfully for the request."); } - var user = userResult.Value; - _logger.LogInformation("Login request for {Username} with id {Id} processed successfully", user.Username, user.Id); - - var sessionResult = await _userSession.OpenSessionAsync(user, loginRequest.DeviceInfo); - - if (sessionResult.IsFailure) - { - _logger.LogError("Failed to open session for user {Username}. Error: {Error}", user.Username, sessionResult.Error); - return StatusCode(500, "An error occurred while creating the session."); - } - - _logger.LogInformation("Session for user {Username} with id {Id} has been opened", user.Username, user.Id); - - return Ok(sessionResult.Value); + return result.ToActionResult(); } } \ No newline at end of file diff --git a/Govor.API/Controllers/Friends/FriendshipController.cs b/Govor.API/Controllers/Friends/FriendshipController.cs index d375717..afbc129 100644 --- a/Govor.API/Controllers/Friends/FriendshipController.cs +++ b/Govor.API/Controllers/Friends/FriendshipController.cs @@ -44,7 +44,7 @@ public class FriendshipController : Controller { _logger.LogWarning(ex, ex.Message); return Forbid(ex.Message); - } + } catch (Exception ex) { _logger.LogError(ex, ex.Message); diff --git a/Govor.API/Govor.API.csproj b/Govor.API/Govor.API.csproj index c109986..1d39339 100644 --- a/Govor.API/Govor.API.csproj +++ b/Govor.API/Govor.API.csproj @@ -1,27 +1,33 @@ - net8.0 disable enable + net10.0 - - - - - - - - + + + + + + + + + + + + - - - + + + + ..\libs\SmartRes.dll + diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index 1ee5ad4..c84e91b 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -7,7 +7,7 @@ using Govor.Application.Authentication.JWT; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; var builder = WebApplication.CreateBuilder(args); @@ -86,28 +86,34 @@ builder.Services.AddGovorDbContext(configuration); // GovorDbContext init builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => +services.AddSwaggerGen(options => { + const string schemeId = "Bearer"; + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Govor API", Version = "v1" }); - options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + options.AddSecurityDefinition(schemeId, new OpenApiSecurityScheme { - Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'", - Name = "Authorization", - In = ParameterLocation.Header, Type = SecuritySchemeType.Http, - Scheme = "bearer" + In = ParameterLocation.Header, + Scheme = "bearer", + BearerFormat = "JWT", + Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'" }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement + options.AddSecurityRequirement(document => { + var requirement = new OpenApiSecurityRequirement { - new OpenApiSecurityScheme { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } - }, - Array.Empty() - } + new OpenApiSecuritySchemeReference(schemeId) + { + Reference = new OpenApiReferenceWithDescription { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + [] + } + }; + return requirement; }); }); @@ -121,7 +127,7 @@ if (!app.Environment.IsDevelopment()) { //app.MapOpenApi(); builder.WebHost.UseUrls("http://0.0.0.0:8080"); - builder.WebHost.UseUrls("http://10.8.0.5:5000"); + //builder.WebHost.UseUrls("http://10.8.0.5:5000"); //builder.WebHost.UseUrls("http://192.168.1.107:8080"); } diff --git a/Govor.API/Properties/launchSettings.json b/Govor.API/Properties/launchSettings.json index 83eb278..bdcb24a 100644 --- a/Govor.API/Properties/launchSettings.json +++ b/Govor.API/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://0.0.0.0:8080;http://localhost:7155;http://10.8.0.5:5000", + "applicationUrl": "http://0.0.0.0:8080;http://localhost:7155", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Govor.Application/Authentication/AuthService.cs b/Govor.Application/Authentication/AuthService.cs index 2f9df25..a5b6b88 100644 --- a/Govor.Application/Authentication/AuthService.cs +++ b/Govor.Application/Authentication/AuthService.cs @@ -6,6 +6,7 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Users; using Microsoft.EntityFrameworkCore; +using SmartRes; namespace Govor.Application.Authentication; @@ -28,18 +29,18 @@ public class AuthService : IAccountService _usernameValidator = usernameValidator; } - public async Task> RegistrationAsync(string name, string password, Invitation invitation) + public async Task> RegistrationAsync(string name, string password, Invitation invitation) { var validationResult = _usernameValidator.Validate(name); if (validationResult.IsFailure) { - return Result.Failure(validationResult.Error); + return Result.Failure(validationResult.Error); } if (await _userNameExistValidator.IsUsernameExistsAsync(name)) { - return Result.Failure(new Error( + return Result.Failure(Error.Conflict( nameof(UserAlreadyExistException), $"User with username '{name}' already exists.")); } @@ -63,12 +64,14 @@ public class AuthService : IAccountService await SetRoleAsync(user, invitation); + // TODO: inv.participantCount -= 1; db.save(); + await _context.SaveChangesAsync(); return user; // Success } - public async Task> LoginAsync(string name, string password) + public async Task> LoginAsync(string name, string password) { var user = await _context.Users .AsNoTracking() @@ -76,14 +79,14 @@ public class AuthService : IAccountService if (user is null) { - return Result.Failure(new Error( + return Result.Failure(Error.NotFound( nameof(UserNotRegisteredException), $"User '{name}' is not registered.")); } if (!_passwordHasher.Verify(password, user.PasswordHash)) { - return Result.Failure(new Error( + return Result.Failure(Error.Failure( nameof(InvalidOperationException), "The password provided is incorrect.")); } diff --git a/Govor.Application/Authentication/IAuthService.cs b/Govor.Application/Authentication/IAuthService.cs index 48816d1..1f508c4 100644 --- a/Govor.Application/Authentication/IAuthService.cs +++ b/Govor.Application/Authentication/IAuthService.cs @@ -1,11 +1,12 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Users; +using SmartRes; namespace Govor.Application.Authentication; public interface IAccountService { - public Task> RegistrationAsync(string name, string password, Invitation invitation); - public Task> LoginAsync(string name, string password); + public Task> RegistrationAsync(string name, string password, Invitation invitation); + public Task> LoginAsync(string name, string password); } \ No newline at end of file diff --git a/Govor.Application/Authentication/IInvitesService.cs b/Govor.Application/Authentication/IInvitesService.cs index 18e030a..1c8ecf3 100644 --- a/Govor.Application/Authentication/IInvitesService.cs +++ b/Govor.Application/Authentication/IInvitesService.cs @@ -1,6 +1,7 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Users; +using SmartRes; namespace Govor.Application.Authentication; @@ -8,5 +9,5 @@ public interface IInvitesService { public Task GetRoleNameAsync(User user); public Task GetRoleNameAsync(Guid sessionId); - public Task> ValidateAsync(string inviteCode); + public Task> ValidateAsync(string inviteCode); } \ No newline at end of file diff --git a/Govor.Application/Authentication/InvitesService.cs b/Govor.Application/Authentication/InvitesService.cs index 1d2a70c..c4b4d60 100644 --- a/Govor.Application/Authentication/InvitesService.cs +++ b/Govor.Application/Authentication/InvitesService.cs @@ -4,6 +4,7 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Users; using Microsoft.EntityFrameworkCore; +using SmartRes; namespace Govor.Application.Authentication; @@ -31,22 +32,24 @@ public class InvitesService : IInvitesService return invitation.IsAdmin ? "Admin" : "User"; } - public async Task> ValidateAsync(string inviteCode) + public async Task> ValidateAsync(string inviteCode) { var invite = await _context.Invitations .Include(s => s.Users) .FirstOrDefaultAsync(s => s.Code == inviteCode); if (invite == null) - return Result.Failure(Error.Null); + return Result.Failure(Error.NotFound("Auth.LinkNotFount","Invitation not found.")); if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count) { invite.IsActive = false; await _context.SaveChangesAsync(); - return Result.Failure(new Error( - "Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}") + return Result.Failure( + Error.Failure( + "Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}" + ) ); } diff --git a/Govor.Application/Friends/FriendRequestCommandService.cs b/Govor.Application/Friends/FriendRequestCommandService.cs index 6656510..66e349e 100644 --- a/Govor.Application/Friends/FriendRequestCommandService.cs +++ b/Govor.Application/Friends/FriendRequestCommandService.cs @@ -5,6 +5,7 @@ using Govor.Domain; using Govor.Domain.Common; using Govor.Domain.Models; using Microsoft.EntityFrameworkCore; +using SmartRes; namespace Govor.Application.Friends; @@ -21,10 +22,10 @@ public class FriendRequestCommandService : IFriendRequestCommandService _privateChatsCreator = privateChatsCreator; } - public async Task> SendAsync(Guid fromUserId, Guid toUserId) + public async Task> SendAsync(Guid fromUserId, Guid toUserId) { if (fromUserId == toUserId) - return Result.Failure(new Error( + return Result.Failure(Error.Failure( "Friendship.Send", "Cannot send a request to self user") ); @@ -50,7 +51,7 @@ public class FriendRequestCommandService : IFriendRequestCommandService friendship.Status == FriendshipStatus.Accepted || friendship.Status == FriendshipStatus.Blocked) { - return Result.Failure(new Error( + return Result.Failure(Error.Conflict( "Friendship.Send", $"The request is already {friendship.Status}") ); @@ -65,24 +66,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService return friendship; } - public async Task> AcceptAsync(Guid requestId, Guid currentUserId) + public async Task> AcceptAsync(Guid requestId, Guid currentUserId) { var friendship = await _context.Friendships.FindAsync(requestId); if (friendship is null) - return Result.Failure(new Error( + return Result.Failure(Error.NotFound( "Friendship.Accept", "Friendship not found! You cant accept request!") ); if (friendship.AddresseeId != currentUserId) - return Result.Failure(new Error( + return Result.Failure(Error.Forbidden( "Friendship.Accept", "You cannot accept this request!") ); if (friendship.Status != FriendshipStatus.Pending) - return Result.Failure(new Error( + return Result.Failure(Error.Forbidden( "Friendship.Accept", "Request is already accepted!") ); @@ -96,24 +97,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService return friendship; } - public async Task> RejectAsync(Guid requestId, Guid currentUserId) + public async Task> RejectAsync(Guid requestId, Guid currentUserId) { var friendship = await _context.Friendships.FindAsync(requestId); if (friendship == null) - return Result.Failure(new Error( + return Result.Failure(Error.NotFound( "Friendship.Reject", "Friendship not found! You cant reject request!") ); if (friendship.AddresseeId != currentUserId) - return Result.Failure(new Error( + return Result.Failure(Error.Forbidden( "Friendship.Reject", "You cannot reject this request!") ); if (friendship.Status != FriendshipStatus.Pending && friendship.Status != FriendshipStatus.Rejected) - return Result.Failure(new Error( + return Result.Failure(Error.Conflict( "Friendship.Reject", $"Request is already {friendship.Status}") ); diff --git a/Govor.Application/Friends/IFriendRequestCommandService.cs b/Govor.Application/Friends/IFriendRequestCommandService.cs index 1ed7d01..7473eae 100644 --- a/Govor.Application/Friends/IFriendRequestCommandService.cs +++ b/Govor.Application/Friends/IFriendRequestCommandService.cs @@ -1,11 +1,12 @@ using Govor.Domain.Common; using Govor.Domain.Models; +using SmartRes; namespace Govor.Application.Friends; public interface IFriendRequestCommandService { - Task> SendAsync(Guid fromUserId, Guid toUserId); - Task> AcceptAsync(Guid requestId, Guid currentUserId); - Task> RejectAsync(Guid requestId, Guid currentUserId); + Task> SendAsync(Guid fromUserId, Guid toUserId); + Task> AcceptAsync(Guid requestId, Guid currentUserId); + Task> RejectAsync(Guid requestId, Guid currentUserId); } diff --git a/Govor.Application/Govor.Application.csproj b/Govor.Application/Govor.Application.csproj index 3fc6ad7..78d9a3a 100644 --- a/Govor.Application/Govor.Application.csproj +++ b/Govor.Application/Govor.Application.csproj @@ -1,23 +1,31 @@  - net8.0 + net10.0 enable enable - + - - - - - - - - + + + + + + + + + + + + + + ..\libs\SmartRes.dll + + diff --git a/Govor.Application/Groups/IGroupService.cs b/Govor.Application/Groups/IGroupService.cs index 830b079..075c365 100644 --- a/Govor.Application/Groups/IGroupService.cs +++ b/Govor.Application/Groups/IGroupService.cs @@ -1,6 +1,7 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Govor.Domain.Models.Users; +using SmartRes; namespace Govor.Application.Groups; @@ -8,9 +9,9 @@ public interface IGroupService { Task GetGroupByIdAsync(Guid groupId); Task CreateGroupAsync(string name, Guid creatorId, IEnumerable initialMemberIds); - Task AddUserToGroupByInvitationAsync(Guid userId, string invitationCode); - Task RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId); - Task DeleteGroupAsync(Guid groupId, Guid userId); + Task> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode); + Task> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId); + Task> DeleteGroupAsync(Guid groupId, Guid userId); Task> GetGroupMembersAsync(Guid groupId); Task>GetUserGroupsAsync(Guid userId); ChatGroup GetGroupByInviteCode(string code); diff --git a/Govor.Application/Infrastructure/AdminsStuff/IInvitationGetter.cs b/Govor.Application/Infrastructure/AdminsStuff/IInvitationGetter.cs index e34c12d..7713287 100644 --- a/Govor.Application/Infrastructure/AdminsStuff/IInvitationGetter.cs +++ b/Govor.Application/Infrastructure/AdminsStuff/IInvitationGetter.cs @@ -1,10 +1,11 @@ using Govor.Domain.Common; using Govor.Domain.Models; +using SmartRes; namespace Govor.Application.Infrastructure.AdminsStuff; public interface IInvitationGetter { Task> GetAllAsync(); - Task> FindByIdAsync(Guid id); + Task> FindByIdAsync(Guid id); } \ No newline at end of file diff --git a/Govor.Application/Infrastructure/AdminsStuff/InvitationGetter.cs b/Govor.Application/Infrastructure/AdminsStuff/InvitationGetter.cs index 961eaa1..4ca7c89 100644 --- a/Govor.Application/Infrastructure/AdminsStuff/InvitationGetter.cs +++ b/Govor.Application/Infrastructure/AdminsStuff/InvitationGetter.cs @@ -3,6 +3,7 @@ using Govor.Domain.Common; using Govor.Domain.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Infrastructure.AdminsStuff; @@ -25,13 +26,13 @@ public class InvitationGetter : IInvitationGetter .ToListAsync(); } - public async Task> FindByIdAsync(Guid id) + public async Task> FindByIdAsync(Guid id) { var res = await _context.Invitations.AsNoTracking() .FirstOrDefaultAsync(iv => iv.Id == id); if (res is null) - return Result.Failure(new Error( + return Result.Failure(Error.NotFound( nameof(InvalidOperationException), "Invitation not found.") ); diff --git a/Govor.Application/Infrastructure/Validators/IUsernameValidator.cs b/Govor.Application/Infrastructure/Validators/IUsernameValidator.cs index 46106c0..8d78d49 100644 --- a/Govor.Application/Infrastructure/Validators/IUsernameValidator.cs +++ b/Govor.Application/Infrastructure/Validators/IUsernameValidator.cs @@ -1,9 +1,10 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.Infrastructure.Validators; public interface IUsernameValidator { - Result Validate(string username); + Result Validate(string username); bool TryValidate(string username); } \ No newline at end of file diff --git a/Govor.Application/Infrastructure/Validators/UsernameValidator.cs b/Govor.Application/Infrastructure/Validators/UsernameValidator.cs index 4e9c531..269beee 100644 --- a/Govor.Application/Infrastructure/Validators/UsernameValidator.cs +++ b/Govor.Application/Infrastructure/Validators/UsernameValidator.cs @@ -3,6 +3,7 @@ using Govor.Application.Authentication.Exceptions; using Govor.Domain.Common.Constants; using Govor.Domain.Common; using Microsoft.Extensions.Configuration; +using SmartRes; namespace Govor.Application.Infrastructure.Validators; @@ -39,52 +40,58 @@ public class UsernameValidator : IUsernameValidator ?? throw new InvalidOperationException("Reserved not set"); } - public Result Validate(string username) + public Result Validate(string username) { if (username.Length < UserConstants.MIN_LENGHT_OF_NAME || username.Length > UserConstants.MAX_LENGHT_OF_NAME) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - $"Username must be between {UserConstants.MIN_LENGHT_OF_NAME} and {UserConstants.MAX_LENGHT_OF_NAME} characters."); + $"Username must be between {UserConstants.MIN_LENGHT_OF_NAME} and {UserConstants.MAX_LENGHT_OF_NAME} characters.") + ); } if (!_usernameRegex.IsMatch(username)) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - "The username must be in Cyrillic and start with a letter."); + "The username must be in Cyrillic and start with a letter.") + ); } if (Regex.IsMatch(username, @"(.)\1{4,}")) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - "Too many repeating characters."); + "Too many repeating characters.") + ); } var normalized = Normalize(username); if (_reserved.Contains(normalized)) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - "This username is reserved."); + "This username is reserved.") + ); } if (_blockedExact.Contains(normalized)) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - "This username is not allowed."); + "This username is not allowed.") + ); } foreach (var banned in _blockedContains) { if (normalized.Contains(banned)) { - return new Error( + return Result.Failure(Error.Validation( ErrorCode, - "Username contains prohibited content."); + "Username contains prohibited content.") + ); } } diff --git a/Govor.Application/PrivateUserChats/IUserPrivateChatsGetterService.cs b/Govor.Application/PrivateUserChats/IUserPrivateChatsGetterService.cs index 412427e..a404ad5 100644 --- a/Govor.Application/PrivateUserChats/IUserPrivateChatsGetterService.cs +++ b/Govor.Application/PrivateUserChats/IUserPrivateChatsGetterService.cs @@ -1,11 +1,12 @@ using Govor.Domain.Common; using Govor.Domain.Models; +using SmartRes; namespace Govor.Application.PrivateUserChats; public interface IUserPrivateChatsGetterService { Task> GetUserChatsAsync(Guid userId); - Task> GetPrivateChatAsync(Guid chatId); + Task> GetPrivateChatAsync(Guid chatId); Task ExistChatAsync(Guid userIdA, Guid userIdB); } \ No newline at end of file diff --git a/Govor.Application/PrivateUserChats/UserPrivateChatsGetter.cs b/Govor.Application/PrivateUserChats/UserPrivateChatsGetter.cs index 9773a2e..e38690a 100644 --- a/Govor.Application/PrivateUserChats/UserPrivateChatsGetter.cs +++ b/Govor.Application/PrivateUserChats/UserPrivateChatsGetter.cs @@ -2,6 +2,7 @@ using Govor.Domain; using Govor.Domain.Common; using Govor.Domain.Models; using Microsoft.EntityFrameworkCore; +using SmartRes; namespace Govor.Application.PrivateUserChats; @@ -22,14 +23,16 @@ public class UserPrivateChatsGetter : IUserPrivateChatsGetterService .ToListAsync(); } - public async Task> GetPrivateChatAsync(Guid chatId) + public async Task> GetPrivateChatAsync(Guid chatId) { var res = await _context.PrivateChats.AsNoTracking() .FirstOrDefaultAsync(p => p.Id == chatId); if (res == null) - return Result.Failure(new Error(nameof(InvalidOperationException), - "PrivateChat not found.") + return Result.Failure( + Error.Failure( + nameof(InvalidOperationException), + "PrivateChat not found.") ); return res; diff --git a/Govor.Application/Profiles/IProfileService.cs b/Govor.Application/Profiles/IProfileService.cs index 16f4a45..c9a2956 100644 --- a/Govor.Application/Profiles/IProfileService.cs +++ b/Govor.Application/Profiles/IProfileService.cs @@ -1,10 +1,11 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.Profiles; public interface IProfileService { - public Task> GetUserProfileAsync(Guid userId); - public Task SetDescription(string description, Guid userId); - public Task SetNewIcon(Guid userId, Guid iconId); + public Task> GetUserProfileAsync(Guid userId); + public Task> SetDescription(string description, Guid userId); + public Task> SetNewIcon(Guid userId, Guid iconId); } diff --git a/Govor.Application/Profiles/ProfileService.cs b/Govor.Application/Profiles/ProfileService.cs index b8f7a02..da575af 100644 --- a/Govor.Application/Profiles/ProfileService.cs +++ b/Govor.Application/Profiles/ProfileService.cs @@ -2,6 +2,7 @@ using Govor.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Profiles; @@ -16,7 +17,7 @@ public class ProfileService : IProfileService _logger = logger; } - public async Task> GetUserProfileAsync(Guid userId) + public async Task> GetUserProfileAsync(Guid userId) { _logger.LogInformation("Getting user {UserId} profile", userId); @@ -34,13 +35,13 @@ public class ProfileService : IProfileService if (profile is null) { - return Result.Failure(CreateNotFoundError(userId)); + return Result.Failure(CreateNotFoundError(userId)); } return profile; } - public async Task SetDescription(string description, Guid userId) + public async Task> SetDescription(string description, Guid userId) { _logger.LogInformation("Updating description for user {UserId}", userId); @@ -56,7 +57,7 @@ public class ProfileService : IProfileService return Result.Success(); } - public async Task SetNewIcon(Guid userId, Guid iconId) + public async Task> SetNewIcon(Guid userId, Guid iconId) { _logger.LogInformation("Updating icon for user {UserId}", userId); @@ -74,5 +75,5 @@ public class ProfileService : IProfileService } private static Error CreateNotFoundError(Guid userId) => - new("Profile.UserNotFound", $"User with ID {userId} was not found."); + Error.NotFound("Profile.UserNotFound", $"User with ID {userId} was not found."); } diff --git a/Govor.Application/PushNotifications/IPushNotificationService.cs b/Govor.Application/PushNotifications/IPushNotificationService.cs index 39c1a39..d81a4d8 100644 --- a/Govor.Application/PushNotifications/IPushNotificationService.cs +++ b/Govor.Application/PushNotifications/IPushNotificationService.cs @@ -1,12 +1,13 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.PushNotifications; public interface IPushNotificationService { - Task SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary? data = null); + Task> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary? data = null); - Task SendToUsersAsync(IEnumerable userIds, string title, string body, string channelId, string tag = "", Dictionary? data = null); + Task> SendToUsersAsync(IEnumerable userIds, string title, string body, string channelId, string tag = "", Dictionary? data = null); - Task SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary? data = null); + Task> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary? data = null); } \ No newline at end of file diff --git a/Govor.Application/PushNotifications/IPushTokenService.cs b/Govor.Application/PushNotifications/IPushTokenService.cs index 57bdca4..70cdf2d 100644 --- a/Govor.Application/PushNotifications/IPushTokenService.cs +++ b/Govor.Application/PushNotifications/IPushTokenService.cs @@ -1,14 +1,15 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.PushNotifications; public interface IPushTokenService { - Task DeactivateTokenBySessionAsync(Guid sessionId); - Task DeactivateAllTokensByUserIdAsync(Guid userId); - Task>> GetStringsActiveTokensAsync(Guid userId); - Task>> GetUsersStringsActiveTokensAsync(IEnumerable userIds); - Task> GetActiveTokenBySessionAsync(Guid sessionId); - Task RemoveTokensAsync(IEnumerable tokens); - Task AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform); + Task> DeactivateTokenBySessionAsync(Guid sessionId); + Task> DeactivateAllTokensByUserIdAsync(Guid userId); + Task, Error>> GetStringsActiveTokensAsync(Guid userId); + Task, Error>> GetUsersStringsActiveTokensAsync(IEnumerable userIds); + Task> GetActiveTokenBySessionAsync(Guid sessionId); + Task> RemoveTokensAsync(IEnumerable tokens); + Task> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform); } \ No newline at end of file diff --git a/Govor.Application/PushNotifications/PushNotificationService.cs b/Govor.Application/PushNotifications/PushNotificationService.cs index 2d7dd61..0f6121c 100644 --- a/Govor.Application/PushNotifications/PushNotificationService.cs +++ b/Govor.Application/PushNotifications/PushNotificationService.cs @@ -2,6 +2,7 @@ using Govor.Application.Interfaces.PushNotifications.Models; using Govor.Application.PushNotifications.Providers; using Govor.Domain.Common; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.PushNotifications; @@ -21,7 +22,7 @@ public class PushNotificationService : IPushNotificationService _logger = logger; } - public async Task SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary? data = null) + public async Task> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary? data = null) { var resultTokens = await _tokenService.GetStringsActiveTokensAsync(userId); if (resultTokens.IsFailure) @@ -37,10 +38,10 @@ public class PushNotificationService : IPushNotificationService return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, userId.ToString()); } - public async Task SendToUsersAsync(IEnumerable userIds, string title, string body, string channelId, string tag = "", Dictionary? data = null) + public async Task> SendToUsersAsync(IEnumerable userIds, string title, string body, string channelId, string tag = "", Dictionary? data = null) { if (userIds == null || !userIds.Any()) - return Result.Failure(new Error("Push.InvalidArgs", "User IDs collection cannot be empty.")); + return Result.Failure(Error.Failure("Push.InvalidArgs", "User IDs collection cannot be empty.")); var tokensResult = await _tokenService.GetUsersStringsActiveTokensAsync(userIds); if (tokensResult.IsFailure) @@ -53,7 +54,7 @@ public class PushNotificationService : IPushNotificationService return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, "bulk_request"); } - public async Task SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary? data = null) + public async Task> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary? data = null) { var tokenResult = await _tokenService.GetActiveTokenBySessionAsync(sessionId); if (tokenResult.IsFailure) @@ -83,7 +84,7 @@ public class PushNotificationService : IPushNotificationService } } - private async Task SendMulticastInternalAsync(List tokens, string title, string body, string channelId, string tag, Dictionary? data, string targetInfo) + private async Task> SendMulticastInternalAsync(List tokens, string title, string body, string channelId, string tag, Dictionary? data, string targetInfo) { try { diff --git a/Govor.Application/PushNotifications/PushTokenService.cs b/Govor.Application/PushNotifications/PushTokenService.cs index 76f1ad2..11b1dea 100644 --- a/Govor.Application/PushNotifications/PushTokenService.cs +++ b/Govor.Application/PushNotifications/PushTokenService.cs @@ -3,6 +3,7 @@ using Govor.Domain.Common; using Govor.Domain.Models.Users; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.PushNotifications; @@ -17,7 +18,7 @@ public class PushTokenService : IPushTokenService _logger = logger; } - public async Task DeactivateTokenBySessionAsync(Guid sessionId) + public async Task> DeactivateTokenBySessionAsync(Guid sessionId) { try { @@ -34,7 +35,7 @@ public class PushTokenService : IPushTokenService } } - public async Task DeactivateAllTokensByUserIdAsync(Guid userId) + public async Task> DeactivateAllTokensByUserIdAsync(Guid userId) { try { @@ -51,7 +52,7 @@ public class PushTokenService : IPushTokenService } } - public async Task>> GetStringsActiveTokensAsync(Guid userId) + public async Task, Error>> GetStringsActiveTokensAsync(Guid userId) { try { @@ -66,11 +67,11 @@ public class PushTokenService : IPushTokenService catch (Exception ex) { _logger.LogError(ex, "Failed to fetch active push tokens for user {UserId}", userId); - return Result>.Failure(ex); + return Result.Failure>(ex); } } - public async Task>> GetUsersStringsActiveTokensAsync(IEnumerable userIds) + public async Task, Error>> GetUsersStringsActiveTokensAsync(IEnumerable userIds) { try { @@ -85,11 +86,11 @@ public class PushTokenService : IPushTokenService catch (Exception ex) { _logger.LogError(ex, "Failed to fetch active push tokens for bulk users"); - return Result>.Failure(ex); + return Result.Failure>(ex); } } - public async Task> GetActiveTokenBySessionAsync(Guid sessionId) + public async Task> GetActiveTokenBySessionAsync(Guid sessionId) { try { @@ -104,11 +105,11 @@ public class PushTokenService : IPushTokenService catch (Exception ex) { _logger.LogError(ex, "Failed to fetch active push token for session {SessionId}", sessionId); - return Result.Failure(ex); + return Result.Failure(ex); } } - public async Task RemoveTokensAsync(IEnumerable tokens) + public async Task> RemoveTokensAsync(IEnumerable tokens) { if (tokens is null || !tokens.Any()) return Result.Success(); @@ -128,11 +129,11 @@ public class PushTokenService : IPushTokenService } } - public async Task AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform) + public async Task> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform) { if (string.IsNullOrWhiteSpace(token)) { - return new Error("PushToken.Empty", "Push token cannot be empty."); + return Result.Failure(Error.Failure("PushToken.Empty", "Push token cannot be empty.")); } var existingToken = await _context.UserPushTokens diff --git a/Govor.Application/Users/UserSessions/IUserSessionOpener.cs b/Govor.Application/Users/UserSessions/IUserSessionOpener.cs index 4f95a7a..3738ce6 100644 --- a/Govor.Application/Users/UserSessions/IUserSessionOpener.cs +++ b/Govor.Application/Users/UserSessions/IUserSessionOpener.cs @@ -1,11 +1,12 @@ using Govor.Domain.Common; using Govor.Domain.Models.Users; +using SmartRes; namespace Govor.Application.Users.UserSessions; public interface IUserSessionOpener { - Task> OpenSessionAsync(User user, string deviceInfo); + Task> OpenSessionAsync(User user, string deviceInfo); } diff --git a/Govor.Application/Users/UserSessions/IUserSessionReader.cs b/Govor.Application/Users/UserSessions/IUserSessionReader.cs index 49432c5..d52680a 100644 --- a/Govor.Application/Users/UserSessions/IUserSessionReader.cs +++ b/Govor.Application/Users/UserSessions/IUserSessionReader.cs @@ -1,8 +1,9 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.Users.UserSessions; public interface IUserSessionReader { - Task>> GetAllSessionsAsync(Guid userId); + Task, Error>> GetAllSessionsAsync(Guid userId); } \ No newline at end of file diff --git a/Govor.Application/Users/UserSessions/IUserSessionRefresher.cs b/Govor.Application/Users/UserSessions/IUserSessionRefresher.cs index 602e79e..b2b6ca4 100644 --- a/Govor.Application/Users/UserSessions/IUserSessionRefresher.cs +++ b/Govor.Application/Users/UserSessions/IUserSessionRefresher.cs @@ -1,8 +1,9 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.Users.UserSessions; public interface IUserSessionRefresher { - Task> RefreshTokenAsync(string refreshToken); + Task> RefreshTokenAsync(string refreshToken); } \ No newline at end of file diff --git a/Govor.Application/Users/UserSessions/IUserSessionRevoker.cs b/Govor.Application/Users/UserSessions/IUserSessionRevoker.cs index 0369cef..6fb1f99 100644 --- a/Govor.Application/Users/UserSessions/IUserSessionRevoker.cs +++ b/Govor.Application/Users/UserSessions/IUserSessionRevoker.cs @@ -1,10 +1,11 @@ using Govor.Domain.Common; +using SmartRes; namespace Govor.Application.Users.UserSessions; public interface IUserSessionRevoker { - Task CloseSessionByIdAsync(Guid sessionId, Guid userId); - Task CloseAllSessionsAsync(Guid userId); + Task> CloseSessionByIdAsync(Guid sessionId, Guid userId); + Task> CloseAllSessionsAsync(Guid userId); } diff --git a/Govor.Application/Users/UserSessions/UserSessionOpener.cs b/Govor.Application/Users/UserSessions/UserSessionOpener.cs index 4788505..8646cf8 100644 --- a/Govor.Application/Users/UserSessions/UserSessionOpener.cs +++ b/Govor.Application/Users/UserSessions/UserSessionOpener.cs @@ -1,9 +1,10 @@ using Govor.Application.Authentication.JWT; using Govor.Domain; -using Govor.Domain.Common; // Путь к вашему Result и Error +using Govor.Domain.Common; using Govor.Domain.Models.Users; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using SmartRes; namespace Govor.Application.Users.UserSessions; @@ -32,7 +33,7 @@ public class UserSessionOpener : IUserSessionOpener _jwtService = jwtService; } - public async Task> OpenSessionAsync(User user, string deviceInfo) + public async Task> OpenSessionAsync(User user, string deviceInfo) { _logger.LogInformation("Opening session for user {UserId} on device '{DeviceInfo}'", user.Id, deviceInfo); @@ -41,7 +42,7 @@ public class UserSessionOpener : IUserSessionOpener if (result.IsFailure) { _logger.LogError("Failed to fetch sessions for user {UserId}: {Error}", user.Id, result.Error.Message); - return Result.Failure(result.Error); + return Result.Failure(result.Error); } var sessions = result.Value; @@ -67,7 +68,7 @@ public class UserSessionOpener : IUserSessionOpener catch (Exception ex) { _logger.LogError(ex, "Database error while opening session for user {UserId}", user.Id); - return Result.Failure(ex); + return Result.Failure(ex); } } diff --git a/Govor.Application/Users/UserSessions/UserSessionReader.cs b/Govor.Application/Users/UserSessions/UserSessionReader.cs index fdbcb68..92de90f 100644 --- a/Govor.Application/Users/UserSessions/UserSessionReader.cs +++ b/Govor.Application/Users/UserSessions/UserSessionReader.cs @@ -3,6 +3,7 @@ using Govor.Domain.Common; using Govor.Domain.Models.Users; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Users.UserSessions; @@ -17,13 +18,14 @@ public class UserSessionReader : IUserSessionReader _logger = logger; } - public async Task>> GetAllSessionsAsync(Guid userId) + public async Task, Error>> GetAllSessionsAsync(Guid userId) { if (userId == Guid.Empty) { - return Result>.Failure(new Error( - "UserSession.InvalidUserId", - "Provided User ID cannot be empty.")); + return Result.Failure>(Error.Conflict("UserSession.InvalidUserId", + "Provided User ID cannot be empty.") + ); + } _logger.LogInformation("Getting all active sessions for user {UserId}", userId); @@ -40,7 +42,7 @@ public class UserSessionReader : IUserSessionReader catch (Exception ex) { _logger.LogError(ex, "Failed to fetch user sessions for user {UserId}", userId); - return Result>.Failure(ex); + return Result.Failure>(ex); } } } \ No newline at end of file diff --git a/Govor.Application/Users/UserSessions/UserSessionRefresher.cs b/Govor.Application/Users/UserSessions/UserSessionRefresher.cs index 1486d16..6c7467b 100644 --- a/Govor.Application/Users/UserSessions/UserSessionRefresher.cs +++ b/Govor.Application/Users/UserSessions/UserSessionRefresher.cs @@ -4,6 +4,7 @@ using Govor.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using SmartRes; namespace Govor.Application.Users.UserSessions; @@ -29,11 +30,11 @@ public class UserSessionRefresher : IUserSessionRefresher _context = context; } - public async Task> RefreshTokenAsync(string refreshToken) + public async Task> RefreshTokenAsync(string refreshToken) { if (string.IsNullOrWhiteSpace(refreshToken)) { - return Result.Failure(new Error("Auth.EmptyToken", "Refresh token cannot be empty.")); + return Result.Failure(Error.Failure("Auth.EmptyToken", "Refresh token cannot be empty.")); } var hashedToken = _jwtTokenHasher.HashToken(refreshToken); @@ -48,13 +49,13 @@ public class UserSessionRefresher : IUserSessionRefresher if (session is null) { _logger.LogWarning("Refresh token session not found for hashed token"); - return Result.Failure(new Error("Auth.InvalidToken", "Invalid refresh token.")); + return Result.Failure(Error.Failure("Auth.InvalidToken", "Invalid refresh token.")); } if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow) { _logger.LogWarning("Attempted to refresh an expired or revoked session: {SessionId}", session.Id); - return Result.Failure(new Error("Auth.InvalidToken", "Refresh token is invalid or expired.")); + return Result.Failure(Error.Failure("Auth.InvalidToken", "Refresh token is invalid or expired.")); } var newAccessToken = await _jwtService.GenerateAccessTokenAsync(session.User, session.Id); @@ -74,7 +75,7 @@ public class UserSessionRefresher : IUserSessionRefresher catch (Exception ex) { _logger.LogError(ex, "Database error occurred during token refresh execution"); - return Result.Failure(ex); + return Result.Failure(ex); } } } diff --git a/Govor.Application/Users/UserSessions/UserSessionRevoker.cs b/Govor.Application/Users/UserSessions/UserSessionRevoker.cs index 4135449..87a6a8a 100644 --- a/Govor.Application/Users/UserSessions/UserSessionRevoker.cs +++ b/Govor.Application/Users/UserSessions/UserSessionRevoker.cs @@ -3,6 +3,7 @@ using Govor.Domain; using Govor.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SmartRes; namespace Govor.Application.Users.UserSessions; @@ -22,7 +23,7 @@ public class UserSessionRevoker : IUserSessionRevoker _logger = logger; } - public async Task CloseSessionByIdAsync(Guid sessionId, Guid userId) + public async Task> CloseSessionByIdAsync(Guid sessionId, Guid userId) { _logger.LogInformation("Attempting to close session {SessionId} for user {UserId}", sessionId, userId); @@ -34,9 +35,13 @@ public class UserSessionRevoker : IUserSessionRevoker if (session is null) { _logger.LogWarning("Active session {SessionId} not found or doesn't belong to user {UserId}", sessionId, userId); - return Result.Failure(new Error( - "UserSession.NotFoundOrUnauthorized", - $"Active session {sessionId} for user {userId} was not found.")); + + return Result.Failure( + Error.NotFound( + "UserSession.NotFoundOrUnauthorized", + $"Active session {sessionId} for user {userId} was not found." + ) + ); } session.IsRevoked = true; @@ -55,7 +60,7 @@ public class UserSessionRevoker : IUserSessionRevoker } } - public async Task CloseAllSessionsAsync(Guid userId) + public async Task> CloseAllSessionsAsync(Guid userId) { _logger.LogInformation("Attempting to close all active sessions for user {UserId}", userId); diff --git a/Govor.ConsoleClient.Tests/Govor.ConsoleClient.Tests.csproj b/Govor.ConsoleClient.Tests/Govor.ConsoleClient.Tests.csproj index 8dc0b55..382cce0 100644 --- a/Govor.ConsoleClient.Tests/Govor.ConsoleClient.Tests.csproj +++ b/Govor.ConsoleClient.Tests/Govor.ConsoleClient.Tests.csproj @@ -25,8 +25,4 @@ - - - - diff --git a/Govor.ConsoleClient/App.cs b/Govor.ConsoleClient/App.cs deleted file mode 100644 index 8b2a59a..0000000 --- a/Govor.ConsoleClient/App.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Govor.ConsoleClient.Services; -using Govor.ConsoleClient.Services.Interfaces; - -namespace Govor.ConsoleClient; - -public class App -{ - private readonly IInputPipeline _inputPipeline; - private readonly ILogger _logger; - - public App(IInputPipeline inputPipeline, ILogger logger) - { - _logger = logger; - _inputPipeline = inputPipeline; - } - - public async Task RunAsync() - { - _logger.Title("Добро пожаловать в консольный клиент Говор!"); - while (true) - { - Console.Write(">> "); - var input = Console.ReadLine(); - if (input == null || input.Trim().ToLower() == "exit") break; - await _inputPipeline.ProcessInputAsync(input); - } - } -} diff --git a/Govor.ConsoleClient/Commands/HelpCommand.cs b/Govor.ConsoleClient/Commands/HelpCommand.cs deleted file mode 100644 index f0fa612..0000000 --- a/Govor.ConsoleClient/Commands/HelpCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Reflection; -using Govor.ConsoleClient.Services; -using Govor.ConsoleClient.Services.Interfaces; -using Microsoft.Extensions.DependencyInjection; - -namespace Govor.ConsoleClient.Commands; - -public class HelpCommand : ICommand -{ - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public HelpCommand(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - public Task ExecuteAsync(CommandContext context) - { - var dispatcher = _serviceProvider.GetRequiredService(); - var commands = dispatcher.GetAllCommands(); - - if (context.Arguments is not null) - { - var command = commands.FirstOrDefault(c => - (c.GetType().GetCustomAttribute()?.Path.Replace("/", "").ToLower() - ?? c.GetType().Name.Replace("Command", "").ToLower()) == context.Arguments.ToLower()); - - if (command != null) - { - _logger.Info($"{context.Arguments} - {command.LongHelp()}"); - } - else - { - _logger.Warn("Unknown command"); - } - } - else - { - _logger.Info("Чтобы получить подробную информацию, напишите /help {command}"); - foreach (var command in commands) - { - var name = command.GetType().GetCustomAttribute()?.Path.Replace("/", "").ToLower() - ?? command.GetType().Name.Replace("Command", "").ToLower(); - - _logger.Log($"{name} - {command.ShortHelp()}"); - } - } - return Task.CompletedTask; - } - - public string LongHelp() => "Необходима для получения информации о доступных командах\nЧтобы получить подробную информацию о команде, напишите /help {command}"; - - public string ShortHelp() => "Необходима для получения информации о доступных командах"; -} diff --git a/Govor.ConsoleClient/Commands/ICommand.cs b/Govor.ConsoleClient/Commands/ICommand.cs deleted file mode 100644 index aa1c844..0000000 --- a/Govor.ConsoleClient/Commands/ICommand.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Govor.ConsoleClient.Services; - -namespace Govor.ConsoleClient.Commands; - -public interface ICommand -{ - Task ExecuteAsync(CommandContext context); - string LongHelp(); - string ShortHelp(); -} - -[AttributeUsage(AttributeTargets.Class)] -public class CommandRouteAttribute : Attribute -{ - public string Path { get; } - public CommandRouteAttribute(string path) => Path = path; -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/IInteractiveCommand.cs b/Govor.ConsoleClient/Commands/IInteractiveCommand.cs deleted file mode 100644 index c13154e..0000000 --- a/Govor.ConsoleClient/Commands/IInteractiveCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Govor.ConsoleClient.Commands; - -public interface IInteractiveCommand : ICommand -{ - Task HandleInputAsync(string input); - bool IsCompleted { get; } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/SendMessageCommand.cs b/Govor.ConsoleClient/Commands/SendMessageCommand.cs deleted file mode 100644 index 0c59601..0000000 --- a/Govor.ConsoleClient/Commands/SendMessageCommand.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Govor.ConsoleClient.Services; - -namespace Govor.ConsoleClient.Commands; - -[CommandRoute("/send")] -public class SendMessageCommand : IInteractiveCommand -{ - private string? _recipient; - private bool _isCompleted; - public bool IsCompleted => _isCompleted; - - public Task ExecuteAsync(CommandContext context) - { - Console.WriteLine("Кому вы хотите отправить сообщение?"); - return Task.CompletedTask; - } - - public async Task HandleInputAsync(string input) - { - if (_recipient == null) - { - _recipient = input; - Console.WriteLine("Введите сообщение:"); - } - else - { - var message = input; - Console.WriteLine($"(Отправка '{message}' пользователю '{_recipient}')"); - _isCompleted = true; - } - - await Task.CompletedTask; - } - - public string LongHelp() - { - return "Отпарвка тестовых сообщений существующему юзеру 2"; - } - - public string ShortHelp() - { - return "Отпарвка тестовых сообщений существующему юзеру"; - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/DependencyInjection.cs b/Govor.ConsoleClient/DependencyInjection.cs deleted file mode 100644 index 15f06f5..0000000 --- a/Govor.ConsoleClient/DependencyInjection.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Reflection; -using Govor.ConsoleClient.Commands; -using Govor.ConsoleClient.Services; -using Govor.ConsoleClient.Services.Extensions; -using Govor.ConsoleClient.Services.Interfaces; -using Govor.ConsoleClient.Services.Middleware; -using Microsoft.Extensions.DependencyInjection; - -namespace Govor.ConsoleClient; - -public static class DependencyInjection -{ - public static IServiceProvider Configure() - { - var services = new ServiceCollection(); - - // Регистрация команд - services.AddCommands(); - - // Сервисы - services.AddApplicationServices(); - - services.AddSingleton(); - - // Middleware - services.AddSingleton(); - - - return services.BuildServiceProvider(); - } - - public static IServiceCollection AddCommands(this IServiceCollection services) - { - var commandTypes = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); - - foreach (var type in commandTypes) - { - services.AddTransient(typeof(ICommand), type); - services.AddTransient(type); // Для прямого внедрения - } - - return services; - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Govor.ConsoleClient.csproj b/Govor.ConsoleClient/Govor.ConsoleClient.csproj deleted file mode 100644 index 1686633..0000000 --- a/Govor.ConsoleClient/Govor.ConsoleClient.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - Exe - net8.0 - 12 - enable - enable - - - - - - - - - - - - - - - - - - - - - diff --git a/Govor.ConsoleClient/Program.cs b/Govor.ConsoleClient/Program.cs deleted file mode 100644 index 491b499..0000000 --- a/Govor.ConsoleClient/Program.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Govor.ConsoleClient; - -internal class Program -{ - //private const string HubBaseUrl = "https://govor-team-govor-88b3.twc1.net/hubs"; - - static async Task Main() - { - Console.Title = "Govor Console Client"; - - var serviceProvider = DependencyInjection.Configure(); - - var app = ActivatorUtilities.CreateInstance(serviceProvider); - await app.RunAsync(); - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/CommandContext.cs b/Govor.ConsoleClient/Services/CommandContext.cs deleted file mode 100644 index d3af695..0000000 --- a/Govor.ConsoleClient/Services/CommandContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Govor.ConsoleClient.Commands; - -namespace Govor.ConsoleClient.Services; - -public class CommandContext -{ - public string Route { get; } - public string? Arguments { get; } - public ICommand Command { get; } - - public CommandContext(string route, string? arguments, ICommand command) - { - Route = route; - Arguments = arguments; - Command = command; - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Extensions/ServiceCollectionExtensions.cs b/Govor.ConsoleClient/Services/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index a095ff0..0000000 --- a/Govor.ConsoleClient/Services/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Govor.ConsoleClient.Services.Implementations; -using Govor.ConsoleClient.Services.Interfaces; -using Microsoft.Extensions.DependencyInjection; - -namespace Govor.ConsoleClient.Services.Extensions; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApplicationServices(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => sp.GetRequiredService()); - return services; - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Implementations/CommandDispatcher.cs b/Govor.ConsoleClient/Services/Implementations/CommandDispatcher.cs deleted file mode 100644 index 52b440d..0000000 --- a/Govor.ConsoleClient/Services/Implementations/CommandDispatcher.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Reflection; -using Govor.ConsoleClient.Commands; -using Govor.ConsoleClient.Services.Interfaces; - -namespace Govor.ConsoleClient.Services.Implementations; - -public class CommandDispatcher : ICommandDispatcher -{ - private readonly Dictionary _commands = new(); - private readonly ILogger _logger; - private readonly IMiddlewarePipeline _pipeline; - - public CommandDispatcher(IEnumerable commands, ILogger logger, IMiddlewarePipeline pipeline) - { - _logger = logger; - _pipeline = pipeline; - - foreach (var command in commands) - { - var route = command.GetType().GetCustomAttribute()?.Path.Replace("/","") - ?? command.GetType().Name.Replace("Command", "").ToLower(); - _commands[route.ToLower()] = command; - } - } - - public async Task DispatchAsync(string input) - { - var args = input.Split(' ', 2); - var cmd = args[0].ToLower(); - if (_commands.TryGetValue(cmd, out var command)) - { - var context = new CommandContext(cmd, args.Length > 1 ? args[1] : null, command); - await _pipeline.ExecuteAsync(context); - return command; - } - else - { - _logger.Warn("Неизвестная команда. Введите '/help'."); - return null; - } - } - - public IEnumerable GetAllCommands() => _commands.Values; -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Implementations/ConsoleLogger.cs b/Govor.ConsoleClient/Services/Implementations/ConsoleLogger.cs deleted file mode 100644 index 86d22c3..0000000 --- a/Govor.ConsoleClient/Services/Implementations/ConsoleLogger.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Govor.ConsoleClient.Services.Interfaces; - -namespace Govor.ConsoleClient.Services.Implementations; - -public class ConsoleLogger : ILogger -{ - public void Log(string message) - { - Console.ResetColor(); - Console.WriteLine(message); - } - - public void Info(string message) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"[INFO] {message}"); - Console.ResetColor(); - } - - public void Warn(string message) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"[WARN] {message}"); - Console.ResetColor(); - } - - public void Error(string message) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"[ERROR] {message}"); - Console.ResetColor(); - } - - public void Title(string message) - { - var upper = message.ToUpper(); - var length = upper.Length + 6; - var border = new string('=', length); - var padded = $"= {upper} ="; - - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine(border); - Console.WriteLine(padded); - Console.WriteLine(border); - Console.ResetColor(); - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Implementations/InputPipeline.cs b/Govor.ConsoleClient/Services/Implementations/InputPipeline.cs deleted file mode 100644 index 996fbe6..0000000 --- a/Govor.ConsoleClient/Services/Implementations/InputPipeline.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Govor.ConsoleClient.Commands; -using Govor.ConsoleClient.Services.Interfaces; - -namespace Govor.ConsoleClient.Services.Implementations; - -public class InputPipeline : IInputPipeline -{ - private readonly ICommandDispatcher _dispatcher; - private readonly ILogger _logger; - private IInteractiveCommand? _activeCommand; - - public InputPipeline(ICommandDispatcher dispatcher, ILogger logger) - { - _dispatcher = dispatcher; - _logger = logger; - } - - public async Task ProcessInputAsync(string input) - { - if (string.IsNullOrWhiteSpace(input)) return; - - if (input.StartsWith("/")) - { - _activeCommand = null; - - var commandInput = input[1..]; - var result = await _dispatcher.DispatchAsync(commandInput); - - // If the command supports interactivity, save it as active - if (result is IInteractiveCommand interactiveCommand && !interactiveCommand.IsCompleted) - { - _activeCommand = interactiveCommand; - } - } - else - { - if (_activeCommand != null) - { - await _activeCommand.HandleInputAsync(input); - - if (_activeCommand.IsCompleted) - _activeCommand = null; - } - else - { - _logger.Info($"Введите /help, чтобы узнать доступные команды!"); - } - } - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Implementations/MiddlewarePipeline.cs b/Govor.ConsoleClient/Services/Implementations/MiddlewarePipeline.cs deleted file mode 100644 index 4cc7da1..0000000 --- a/Govor.ConsoleClient/Services/Implementations/MiddlewarePipeline.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Govor.ConsoleClient.Services.Interfaces; -using Govor.ConsoleClient.Services.Middleware; - -namespace Govor.ConsoleClient.Services.Implementations; - -public delegate Task CommandMiddleware(CommandContext context, Func next); - -public class MiddlewarePipeline : IMiddlewarePipeline -{ - private readonly IList _middlewares; - - public MiddlewarePipeline(IEnumerable middlewares) - { - _middlewares = middlewares.ToList(); - } - - public Task ExecuteAsync(CommandContext context) - { - return InvokeNext(0, context); - } - - private Task InvokeNext(int index, CommandContext context) - { - if (index < _middlewares.Count) - { - return _middlewares[index].InvokeAsync(context, () => InvokeNext(index + 1, context)); - } - return context.Command.ExecuteAsync(context); - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Interfaces/ICommandDispatcher.cs b/Govor.ConsoleClient/Services/Interfaces/ICommandDispatcher.cs deleted file mode 100644 index 370adab..0000000 --- a/Govor.ConsoleClient/Services/Interfaces/ICommandDispatcher.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Govor.ConsoleClient.Commands; - -namespace Govor.ConsoleClient.Services.Interfaces; - -public interface ICommandDispatcher -{ - Task DispatchAsync(string input); - IEnumerable GetAllCommands(); -} diff --git a/Govor.ConsoleClient/Services/Interfaces/IInputPipeline.cs b/Govor.ConsoleClient/Services/Interfaces/IInputPipeline.cs deleted file mode 100644 index be8fd92..0000000 --- a/Govor.ConsoleClient/Services/Interfaces/IInputPipeline.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Govor.ConsoleClient.Services.Interfaces; - -public interface IInputPipeline -{ - Task ProcessInputAsync(string input); -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Interfaces/ILogger.cs b/Govor.ConsoleClient/Services/Interfaces/ILogger.cs deleted file mode 100644 index 15136b0..0000000 --- a/Govor.ConsoleClient/Services/Interfaces/ILogger.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Govor.ConsoleClient.Services.Interfaces; - -public interface ILogger -{ - void Log(string message); - void Info(string message); - void Warn(string message); - void Error(string message); - void Title(string title); -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Interfaces/IMiddlewarePipeline.cs b/Govor.ConsoleClient/Services/Interfaces/IMiddlewarePipeline.cs deleted file mode 100644 index b33e96a..0000000 --- a/Govor.ConsoleClient/Services/Interfaces/IMiddlewarePipeline.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Govor.ConsoleClient.Services.Interfaces; - -public interface IMiddlewarePipeline -{ - Task ExecuteAsync(CommandContext context); -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs b/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs deleted file mode 100644 index b105e77..0000000 --- a/Govor.ConsoleClient/Services/Middleware/ExceptionHandlingMiddleware.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Govor.ConsoleClient.Services.Interfaces; - -namespace Govor.ConsoleClient.Services.Middleware; - -public class ExceptionHandlingMiddleware : ICommandMiddleware -{ - private readonly ILogger _logger; - - public ExceptionHandlingMiddleware(ILogger logger) - { - _logger = logger; - } - - public async Task InvokeAsync(CommandContext context, Func next) - { - try - { - await next(); - } - catch (Exception ex) - { - _logger.Error($"Произошла ошибка при выполнении команды '{context?.Route}': {ex.Message}"); - } - } -} \ No newline at end of file diff --git a/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs b/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs deleted file mode 100644 index e441906..0000000 --- a/Govor.ConsoleClient/Services/Middleware/ICommandMiddleware.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Govor.ConsoleClient.Services.Middleware; - -public interface ICommandMiddleware -{ - Task InvokeAsync(CommandContext context, Func next); -} \ No newline at end of file diff --git a/Govor.ConsoleClient/appsettings.json b/Govor.ConsoleClient/appsettings.json deleted file mode 100644 index 3cc9e3f..0000000 --- a/Govor.ConsoleClient/appsettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "BaseUrl": "https://govor-team-govor-88b3.twc1.net" -} \ No newline at end of file diff --git a/Govor.Contracts/Govor.Contracts.csproj b/Govor.Contracts/Govor.Contracts.csproj index ed3cb48..bbc76cb 100644 --- a/Govor.Contracts/Govor.Contracts.csproj +++ b/Govor.Contracts/Govor.Contracts.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable @@ -11,6 +11,6 @@ - + diff --git a/Govor.Domain/Common/Error.cs b/Govor.Domain/Common/Error.cs index e49d9f4..242be7a 100644 --- a/Govor.Domain/Common/Error.cs +++ b/Govor.Domain/Common/Error.cs @@ -1,9 +1,16 @@ namespace Govor.Domain.Common; - -public record Error(string Code, string Message) +public record Error(string Code, string Message, ErrorType Type, Dictionary? Errors = null) { - public static readonly Error None = new(string.Empty, string.Empty); - public static readonly Error Null = new("NULL", "Value cannot be null."); + public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound); + + public static Error Validation(string code, string message, Dictionary? errors = null) => + new(code, message, ErrorType.Validation, errors); + + public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict); + public static Error Unauthorized(string code, string message) => new(code, message, ErrorType.Unauthorized); + public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden); + public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure); + public override string ToString() => $"{Code}: {Message}"; } \ No newline at end of file diff --git a/Govor.Domain/Common/ErrorType.cs b/Govor.Domain/Common/ErrorType.cs new file mode 100644 index 0000000..ac228b8 --- /dev/null +++ b/Govor.Domain/Common/ErrorType.cs @@ -0,0 +1,11 @@ +namespace Govor.Domain.Common; + +public enum ErrorType +{ + Failure = 0, // (400 Bad Request) + Validation = 1, // (400 Bad Request / 422 Unprocessable) + NotFound = 2, // (404 Not Found) + Conflict = 3, // (409 Conflict) + Unauthorized = 4, // (401 Unauthorized) + Forbidden = 5 // (403 Forbidden) +} \ No newline at end of file diff --git a/Govor.Domain/Common/Result.cs b/Govor.Domain/Common/Result.cs index cfcf3a1..4018144 100644 --- a/Govor.Domain/Common/Result.cs +++ b/Govor.Domain/Common/Result.cs @@ -1,49 +1,20 @@ +using SmartRes; + namespace Govor.Domain.Common; -public class Result +public static class Result { - protected Result(bool isSuccess, Error error) - { - if (isSuccess && error != Error.None || !isSuccess && error == Error.None) - { - throw new ArgumentException("Invalid error state", nameof(error)); - } + // Для методов, возвращающих значение + public static Result Success(T value) => Result.Success(value); + public static Result Failure(Error error) => Result.Failure(error); - IsSuccess = isSuccess; - Error = error; - } - - public bool IsSuccess { get; } - public bool IsFailure => !IsSuccess; - public Error Error { get; } - - public static Result Success() => new(true, Error.None); - public static Result Failure(Error error) => new(false, error); + // Для методов, которые раньше возвращали просто Result (без T) + public static Result Success() => Result.Success(Unit.Value); + public static Result Failure(Error error) => Result.Failure(error); - public static Result Failure(Exception ex) => new(false, new Error(ex.GetType().Name, ex.Message)); - - public static implicit operator Result(Error error) => Failure(error); -} - -public class Result : Result -{ - private readonly T? _value; - - private Result(T? value, bool isSuccess, Error error) : base(isSuccess, error) - { - _value = value; - } - - public T Value => IsSuccess - ? _value! - : throw new InvalidOperationException("The value of a failure result cannot be accessed."); - - public static Result Success(T value) => new(value, true, Error.None); - public static new Result Failure(Error error) => new(default, false, error); - public static new Result Failure(Exception ex) => new(default, false, new Error(ex.GetType().Name, ex.Message)); - - public static implicit operator Result(T value) => Success(value); - - public static implicit operator Result(Error error) => Failure(error); - public static implicit operator T(Result result) => result.Value; + public static Result Failure(Exception ex) => + Result.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure)); + + public static Result Failure(Exception ex) => + Result.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure)); } \ No newline at end of file diff --git a/Govor.Domain/Govor.Domain.csproj b/Govor.Domain/Govor.Domain.csproj index 0c11df6..cbaea9f 100644 --- a/Govor.Domain/Govor.Domain.csproj +++ b/Govor.Domain/Govor.Domain.csproj @@ -1,14 +1,20 @@  - net8.0 enable enable + net10.0 - - - + + + + + + + + ..\libs\SmartRes.dll + diff --git a/Govor.sln b/Govor.sln index adbac74..fbf82f2 100644 --- a/Govor.sln +++ b/Govor.sln @@ -4,8 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API.Tests", "Govor.AP EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API", "Govor.API\Govor.API.csproj", "{EA8F272F-4276-438A-9DEA-C58860A440AE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.ConsoleClient", "Govor.ConsoleClient\Govor.ConsoleClient.csproj", "{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{114F53C1-B0AB-4BA0-9E36-0E811D1B3776}" @@ -25,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.ConsoleClient.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Domain", "Govor.Domain\Govor.Domain.csproj", "{868EAA88-A8E8-4508-B264-1AE3576D47C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartRes", "..\SmartRes\SmartRes\SmartRes.csproj", "{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,10 +39,6 @@ Global {EA8F272F-4276-438A-9DEA-C58860A440AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA8F272F-4276-438A-9DEA-C58860A440AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA8F272F-4276-438A-9DEA-C58860A440AE}.Release|Any CPU.Build.0 = Release|Any CPU - {F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Release|Any CPU.Build.0 = Release|Any CPU {4E94907F-BE20-42A6-AB15-637850FEAD11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E94907F-BE20-42A6-AB15-637850FEAD11}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E94907F-BE20-42A6-AB15-637850FEAD11}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -67,11 +63,14 @@ Global {868EAA88-A8E8-4508-B264-1AE3576D47C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {868EAA88-A8E8-4508-B264-1AE3576D47C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {868EAA88-A8E8-4508-B264-1AE3576D47C1}.Release|Any CPU.Build.0 = Release|Any CPU + {3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {15031CBD-F319-4755-BA91-B86F20BD8E37} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4} {EA8F272F-4276-438A-9DEA-C58860A440AE} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776} - {F4535DC3-BDFB-4EB2-B259-F92B6BBB535B} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776} {4E94907F-BE20-42A6-AB15-637850FEAD11} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776} {FC5EDCA8-FD58-4078-8FB1-2BDBB2F6CA3E} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776} {F56A64DF-2938-4BE0-83F2-B86429F19259} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4} diff --git a/global.json b/global.json index dad2db5..a11f48e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "10.0.0", "rollForward": "latestMajor", "allowPrerelease": true } diff --git a/libs/SmartRes.dll b/libs/SmartRes.dll new file mode 100644 index 0000000000000000000000000000000000000000..e2c071e04405543715ea0a2a1261ecd125d6aa6d GIT binary patch literal 10752 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 literal 0 HcmV?d00001