Switch to SmartRes Result/Error, upgrade to .NET 10

Migrate codebase to use SmartRes Result<T, Error> (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).
This commit is contained in:
Artemy
2026-07-25 20:20:45 +07:00
parent 6d1c53beeb
commit 3c785d5c89
65 changed files with 376 additions and 838 deletions
@@ -98,7 +98,7 @@ public static class ConfigurationProgramExtensions
// Auto Mapper // Auto Mapper
services.AddAutoMapper(typeof(MappingProfile)); services.AddAutoMapper(op => { }, typeof(MappingProfile));
services.AddScoped<IHubUserAccessor, HubUserAccessor>(); services.AddScoped<IHubUserAccessor, HubUserAccessor>();
@@ -111,52 +111,26 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IProfileService, ProfileService>(); services.AddScoped<IProfileService, ProfileService>();
} }
public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration) public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration)
{ {
var useMySql = configuration.GetValue<bool>("UseMySql"); services.AddDbContext<GovorDbContext>(options =>
if (useMySql)
{ {
services.AddDbContext<GovorDbContext>(options => options.UseNpgsql(
{ configuration.GetConnectionString(nameof(GovorDbContext)),
var connectionString = configuration.GetConnectionString(nameof(GovorDbContext)); npgsqlOptions =>
{
// retry for transient failures
npgsqlOptions.EnableRetryOnFailure(
5,
TimeSpan.FromSeconds(5),
null);
});
options.UseMySql( //options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
connectionString,
new MySqlServerVersion(new Version(8, 0, 21)), options.EnableSensitiveDataLogging();
mySqlOptions => options.EnableDetailedErrors();
{ });
mySqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null);
});
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
});
}
else
{
services.AddDbContext<GovorDbContext>(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();
});
}
} }
} }
@@ -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<T>(this Result<T, Error> 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"
};
}
@@ -1,9 +1,10 @@
using Govor.API.Common.Extensions;
using Govor.Application.Authentication; using Govor.Application.Authentication;
using Govor.Application.Authentication.Exceptions;
using Govor.Application.Users.UserSessions; using Govor.Application.Users.UserSessions;
using Govor.Contracts.Requests; using Govor.Contracts.Requests;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using SmartRes;
namespace Govor.API.Controllers.Authentication; namespace Govor.API.Controllers.Authentication;
@@ -30,82 +31,47 @@ public class AuthController : Controller
} }
[HttpPost("register")] // api/auth/register [HttpPost("register")] // api/auth/register
public async Task<IActionResult> Register([FromBody] RegistrationRequest registrationRequest) public async Task<IActionResult> Register([FromBody] RegistrationRequest request)
{ {
_logger.LogInformation("Processing registration request for: {Name}", request.Name);
var inviteResult = await _invitesService.ValidateAsync(registrationRequest.InviteLink);
if (!inviteResult.IsSuccess) 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, _logger.LogWarning("Registration pipeline failed. Error: {Code} - {Message}",
inviteResult.Error); result.Error.Code, result.Error.Message);
return BadRequest($"Invite link invalid: {inviteResult.Error.Message}"); }
else
{
_logger.LogInformation("Session opened successfully for the request.");
} }
var userResult = await _accountService.RegistrationAsync( return result.ToActionResult();
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);
} }
[HttpPost("login")] // api/auth/login [HttpPost("login")] // api/auth/login
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest) public async Task<IActionResult> 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); _logger.LogWarning("Login pipeline failed. Error: {Code} - {Message}",
result.Error.Code, result.Error.Message);
return userResult.Error.Code switch }
{ else
nameof(UserNotRegisteredException) => BadRequest("Login failed: user does not exist."), {
nameof(InvalidOperationException) => BadRequest("Login failed: username or password is incorrect."), _logger.LogInformation("Session opened successfully for the request.");
_ => BadRequest($"Login failed: {userResult.Error.Message}")
};
} }
var user = userResult.Value; return result.ToActionResult();
_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);
} }
} }
@@ -44,7 +44,7 @@ public class FriendshipController : Controller
{ {
_logger.LogWarning(ex, ex.Message); _logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message); return Forbid(ex.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, ex.Message); _logger.LogError(ex, ex.Message);
+18 -12
View File
@@ -1,27 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" /> <PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="FirebaseAdmin" Version="3.6.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageReference Include="NSwag.AspNetCore" Version="14.4.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.4.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Microting.EntityFrameworkCore.MySql" Version="10.0.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
<ProjectReference Include="..\Govor.Application\Govor.Application.csproj" /> <ProjectReference Include="..\Govor.Application\Govor.Application.csproj" />
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" /> <ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
<ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" /> <ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" />
+20 -14
View File
@@ -7,7 +7,7 @@ using Govor.Application.Authentication.JWT;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -86,28 +86,34 @@ builder.Services.AddGovorDbContext(configuration); // GovorDbContext init
builder.Services.AddEndpointsApiExplorer(); 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.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, 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" } new OpenApiSecuritySchemeReference(schemeId)
}, {
Array.Empty<string>() Reference = new OpenApiReferenceWithDescription { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
} },
[]
}
};
return requirement;
}); });
}); });
@@ -121,7 +127,7 @@ if (!app.Environment.IsDevelopment())
{ {
//app.MapOpenApi(); //app.MapOpenApi();
builder.WebHost.UseUrls("http://0.0.0.0:8080"); 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"); //builder.WebHost.UseUrls("http://192.168.1.107:8080");
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "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": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -6,6 +6,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Authentication; namespace Govor.Application.Authentication;
@@ -28,18 +29,18 @@ public class AuthService : IAccountService
_usernameValidator = usernameValidator; _usernameValidator = usernameValidator;
} }
public async Task<Result<User>> RegistrationAsync(string name, string password, Invitation invitation) public async Task<Result<User, Error>> RegistrationAsync(string name, string password, Invitation invitation)
{ {
var validationResult = _usernameValidator.Validate(name); var validationResult = _usernameValidator.Validate(name);
if (validationResult.IsFailure) if (validationResult.IsFailure)
{ {
return Result<User>.Failure(validationResult.Error); return Result.Failure<User>(validationResult.Error);
} }
if (await _userNameExistValidator.IsUsernameExistsAsync(name)) if (await _userNameExistValidator.IsUsernameExistsAsync(name))
{ {
return Result<User>.Failure(new Error( return Result.Failure<User>(Error.Conflict(
nameof(UserAlreadyExistException), nameof(UserAlreadyExistException),
$"User with username '{name}' already exists.")); $"User with username '{name}' already exists."));
} }
@@ -63,12 +64,14 @@ public class AuthService : IAccountService
await SetRoleAsync(user, invitation); await SetRoleAsync(user, invitation);
// TODO: inv.participantCount -= 1; db.save();
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return user; // Success return user; // Success
} }
public async Task<Result<User>> LoginAsync(string name, string password) public async Task<Result<User, Error>> LoginAsync(string name, string password)
{ {
var user = await _context.Users var user = await _context.Users
.AsNoTracking() .AsNoTracking()
@@ -76,14 +79,14 @@ public class AuthService : IAccountService
if (user is null) if (user is null)
{ {
return Result<User>.Failure(new Error( return Result.Failure<User>(Error.NotFound(
nameof(UserNotRegisteredException), nameof(UserNotRegisteredException),
$"User '{name}' is not registered.")); $"User '{name}' is not registered."));
} }
if (!_passwordHasher.Verify(password, user.PasswordHash)) if (!_passwordHasher.Verify(password, user.PasswordHash))
{ {
return Result<User>.Failure(new Error( return Result.Failure<User>(Error.Failure(
nameof(InvalidOperationException), nameof(InvalidOperationException),
"The password provided is incorrect.")); "The password provided is incorrect."));
} }
@@ -1,11 +1,12 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Authentication; namespace Govor.Application.Authentication;
public interface IAccountService public interface IAccountService
{ {
public Task<Result<User>> RegistrationAsync(string name, string password, Invitation invitation); public Task<Result<User, Error>> RegistrationAsync(string name, string password, Invitation invitation);
public Task<Result<User>> LoginAsync(string name, string password); public Task<Result<User, Error>> LoginAsync(string name, string password);
} }
@@ -1,6 +1,7 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Authentication; namespace Govor.Application.Authentication;
@@ -8,5 +9,5 @@ public interface IInvitesService
{ {
public Task<string> GetRoleNameAsync(User user); public Task<string> GetRoleNameAsync(User user);
public Task<string> GetRoleNameAsync(Guid sessionId); public Task<string> GetRoleNameAsync(Guid sessionId);
public Task<Result<Invitation>> ValidateAsync(string inviteCode); public Task<Result<Invitation, Error>> ValidateAsync(string inviteCode);
} }
@@ -4,6 +4,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Authentication; namespace Govor.Application.Authentication;
@@ -31,22 +32,24 @@ public class InvitesService : IInvitesService
return invitation.IsAdmin ? "Admin" : "User"; return invitation.IsAdmin ? "Admin" : "User";
} }
public async Task<Result<Invitation>> ValidateAsync(string inviteCode) public async Task<Result<Invitation, Error>> ValidateAsync(string inviteCode)
{ {
var invite = await _context.Invitations var invite = await _context.Invitations
.Include(s => s.Users) .Include(s => s.Users)
.FirstOrDefaultAsync(s => s.Code == inviteCode); .FirstOrDefaultAsync(s => s.Code == inviteCode);
if (invite == null) if (invite == null)
return Result<Invitation>.Failure(Error.Null); return Result.Failure<Invitation>(Error.NotFound("Auth.LinkNotFount","Invitation not found."));
if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count) if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count)
{ {
invite.IsActive = false; invite.IsActive = false;
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return Result<Invitation>.Failure(new Error( return Result.Failure<Invitation>(
"Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}") Error.Failure(
"Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}"
)
); );
} }
@@ -5,6 +5,7 @@ using Govor.Domain;
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Friends; namespace Govor.Application.Friends;
@@ -21,10 +22,10 @@ public class FriendRequestCommandService : IFriendRequestCommandService
_privateChatsCreator = privateChatsCreator; _privateChatsCreator = privateChatsCreator;
} }
public async Task<Result<Friendship>> SendAsync(Guid fromUserId, Guid toUserId) public async Task<Result<Friendship, Error>> SendAsync(Guid fromUserId, Guid toUserId)
{ {
if (fromUserId == toUserId) if (fromUserId == toUserId)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Failure(
"Friendship.Send", "Friendship.Send",
"Cannot send a request to self user") "Cannot send a request to self user")
); );
@@ -50,7 +51,7 @@ public class FriendRequestCommandService : IFriendRequestCommandService
friendship.Status == FriendshipStatus.Accepted || friendship.Status == FriendshipStatus.Accepted ||
friendship.Status == FriendshipStatus.Blocked) friendship.Status == FriendshipStatus.Blocked)
{ {
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Conflict(
"Friendship.Send", "Friendship.Send",
$"The request is already {friendship.Status}") $"The request is already {friendship.Status}")
); );
@@ -65,24 +66,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService
return friendship; return friendship;
} }
public async Task<Result<Friendship>> AcceptAsync(Guid requestId, Guid currentUserId) public async Task<Result<Friendship, Error>> AcceptAsync(Guid requestId, Guid currentUserId)
{ {
var friendship = await _context.Friendships.FindAsync(requestId); var friendship = await _context.Friendships.FindAsync(requestId);
if (friendship is null) if (friendship is null)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.NotFound(
"Friendship.Accept", "Friendship.Accept",
"Friendship not found! You cant accept request!") "Friendship not found! You cant accept request!")
); );
if (friendship.AddresseeId != currentUserId) if (friendship.AddresseeId != currentUserId)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Accept", "Friendship.Accept",
"You cannot accept this request!") "You cannot accept this request!")
); );
if (friendship.Status != FriendshipStatus.Pending) if (friendship.Status != FriendshipStatus.Pending)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Accept", "Friendship.Accept",
"Request is already accepted!") "Request is already accepted!")
); );
@@ -96,24 +97,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService
return friendship; return friendship;
} }
public async Task<Result<Friendship>> RejectAsync(Guid requestId, Guid currentUserId) public async Task<Result<Friendship, Error>> RejectAsync(Guid requestId, Guid currentUserId)
{ {
var friendship = await _context.Friendships.FindAsync(requestId); var friendship = await _context.Friendships.FindAsync(requestId);
if (friendship == null) if (friendship == null)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.NotFound(
"Friendship.Reject", "Friendship.Reject",
"Friendship not found! You cant reject request!") "Friendship not found! You cant reject request!")
); );
if (friendship.AddresseeId != currentUserId) if (friendship.AddresseeId != currentUserId)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Reject", "Friendship.Reject",
"You cannot reject this request!") "You cannot reject this request!")
); );
if (friendship.Status != FriendshipStatus.Pending && friendship.Status != FriendshipStatus.Rejected) if (friendship.Status != FriendshipStatus.Pending && friendship.Status != FriendshipStatus.Rejected)
return Result<Friendship>.Failure(new Error( return Result.Failure<Friendship>(Error.Conflict(
"Friendship.Reject", "Friendship.Reject",
$"Request is already {friendship.Status}") $"Request is already {friendship.Status}")
); );
@@ -1,11 +1,12 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.Friends; namespace Govor.Application.Friends;
public interface IFriendRequestCommandService public interface IFriendRequestCommandService
{ {
Task<Result<Friendship>> SendAsync(Guid fromUserId, Guid toUserId); Task<Result<Friendship, Error>> SendAsync(Guid fromUserId, Guid toUserId);
Task<Result<Friendship>> AcceptAsync(Guid requestId, Guid currentUserId); Task<Result<Friendship, Error>> AcceptAsync(Guid requestId, Guid currentUserId);
Task<Result<Friendship>> RejectAsync(Guid requestId, Guid currentUserId); Task<Result<Friendship, Error>> RejectAsync(Guid requestId, Guid currentUserId);
} }
+18 -10
View File
@@ -1,23 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="FirebaseAdmin" Version="3.4.0" /> <PackageReference Include="FirebaseAdmin" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="11.0.0-preview.1.26104.118" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version = "8.19.2"/>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.2" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
<ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" /> <ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" />
</ItemGroup> </ItemGroup>
+4 -3
View File
@@ -1,6 +1,7 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Groups; namespace Govor.Application.Groups;
@@ -8,9 +9,9 @@ public interface IGroupService
{ {
Task<ChatGroup> GetGroupByIdAsync(Guid groupId); Task<ChatGroup> GetGroupByIdAsync(Guid groupId);
Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds); Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds);
Task<Result> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode); Task<Result<Unit, Error>> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode);
Task<Result> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId); Task<Result<Unit, Error>> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId);
Task<Result> DeleteGroupAsync(Guid groupId, Guid userId); Task<Result<Unit, Error>> DeleteGroupAsync(Guid groupId, Guid userId);
Task<List<User>> GetGroupMembersAsync(Guid groupId); Task<List<User>> GetGroupMembersAsync(Guid groupId);
Task<List<ChatGroup>>GetUserGroupsAsync(Guid userId); Task<List<ChatGroup>>GetUserGroupsAsync(Guid userId);
ChatGroup GetGroupByInviteCode(string code); ChatGroup GetGroupByInviteCode(string code);
@@ -1,10 +1,11 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.Infrastructure.AdminsStuff; namespace Govor.Application.Infrastructure.AdminsStuff;
public interface IInvitationGetter public interface IInvitationGetter
{ {
Task<List<Invitation>> GetAllAsync(); Task<List<Invitation>> GetAllAsync();
Task<Result<Invitation>> FindByIdAsync(Guid id); Task<Result<Invitation, Error>> FindByIdAsync(Guid id);
} }
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Infrastructure.AdminsStuff; namespace Govor.Application.Infrastructure.AdminsStuff;
@@ -25,13 +26,13 @@ public class InvitationGetter : IInvitationGetter
.ToListAsync(); .ToListAsync();
} }
public async Task<Result<Invitation>> FindByIdAsync(Guid id) public async Task<Result<Invitation, Error>> FindByIdAsync(Guid id)
{ {
var res = await _context.Invitations.AsNoTracking() var res = await _context.Invitations.AsNoTracking()
.FirstOrDefaultAsync(iv => iv.Id == id); .FirstOrDefaultAsync(iv => iv.Id == id);
if (res is null) if (res is null)
return Result<Invitation>.Failure(new Error( return Result.Failure<Invitation>(Error.NotFound(
nameof(InvalidOperationException), nameof(InvalidOperationException),
"Invitation not found.") "Invitation not found.")
); );
@@ -1,9 +1,10 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Infrastructure.Validators; namespace Govor.Application.Infrastructure.Validators;
public interface IUsernameValidator public interface IUsernameValidator
{ {
Result Validate(string username); Result<Unit, Error> Validate(string username);
bool TryValidate(string username); bool TryValidate(string username);
} }
@@ -3,6 +3,7 @@ using Govor.Application.Authentication.Exceptions;
using Govor.Domain.Common.Constants; using Govor.Domain.Common.Constants;
using Govor.Domain.Common; using Govor.Domain.Common;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using SmartRes;
namespace Govor.Application.Infrastructure.Validators; namespace Govor.Application.Infrastructure.Validators;
@@ -39,52 +40,58 @@ public class UsernameValidator : IUsernameValidator
?? throw new InvalidOperationException("Reserved not set"); ?? throw new InvalidOperationException("Reserved not set");
} }
public Result Validate(string username) public Result<Unit, Error> Validate(string username)
{ {
if (username.Length < UserConstants.MIN_LENGHT_OF_NAME || username.Length > UserConstants.MAX_LENGHT_OF_NAME) if (username.Length < UserConstants.MIN_LENGHT_OF_NAME || username.Length > UserConstants.MAX_LENGHT_OF_NAME)
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, 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)) if (!_usernameRegex.IsMatch(username))
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, 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,}")) if (Regex.IsMatch(username, @"(.)\1{4,}"))
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, ErrorCode,
"Too many repeating characters."); "Too many repeating characters.")
);
} }
var normalized = Normalize(username); var normalized = Normalize(username);
if (_reserved.Contains(normalized)) if (_reserved.Contains(normalized))
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, ErrorCode,
"This username is reserved."); "This username is reserved.")
);
} }
if (_blockedExact.Contains(normalized)) if (_blockedExact.Contains(normalized))
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, ErrorCode,
"This username is not allowed."); "This username is not allowed.")
);
} }
foreach (var banned in _blockedContains) foreach (var banned in _blockedContains)
{ {
if (normalized.Contains(banned)) if (normalized.Contains(banned))
{ {
return new Error( return Result.Failure(Error.Validation(
ErrorCode, ErrorCode,
"Username contains prohibited content."); "Username contains prohibited content.")
);
} }
} }
@@ -1,11 +1,12 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.PrivateUserChats; namespace Govor.Application.PrivateUserChats;
public interface IUserPrivateChatsGetterService public interface IUserPrivateChatsGetterService
{ {
Task<List<PrivateChat>> GetUserChatsAsync(Guid userId); Task<List<PrivateChat>> GetUserChatsAsync(Guid userId);
Task<Result<PrivateChat>> GetPrivateChatAsync(Guid chatId); Task<Result<PrivateChat, Error>> GetPrivateChatAsync(Guid chatId);
Task<bool> ExistChatAsync(Guid userIdA, Guid userIdB); Task<bool> ExistChatAsync(Guid userIdA, Guid userIdB);
} }
@@ -2,6 +2,7 @@ using Govor.Domain;
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models; using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.PrivateUserChats; namespace Govor.Application.PrivateUserChats;
@@ -22,14 +23,16 @@ public class UserPrivateChatsGetter : IUserPrivateChatsGetterService
.ToListAsync(); .ToListAsync();
} }
public async Task<Result<PrivateChat>> GetPrivateChatAsync(Guid chatId) public async Task<Result<PrivateChat, Error>> GetPrivateChatAsync(Guid chatId)
{ {
var res = await _context.PrivateChats.AsNoTracking() var res = await _context.PrivateChats.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == chatId); .FirstOrDefaultAsync(p => p.Id == chatId);
if (res == null) if (res == null)
return Result<PrivateChat>.Failure(new Error(nameof(InvalidOperationException), return Result.Failure<PrivateChat>(
"PrivateChat not found.") Error.Failure(
nameof(InvalidOperationException),
"PrivateChat not found.")
); );
return res; return res;
@@ -1,10 +1,11 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Profiles; namespace Govor.Application.Profiles;
public interface IProfileService public interface IProfileService
{ {
public Task<Result<UserProfile>> GetUserProfileAsync(Guid userId); public Task<Result<UserProfile, Error>> GetUserProfileAsync(Guid userId);
public Task<Result> SetDescription(string description, Guid userId); public Task<Result<Unit, Error>> SetDescription(string description, Guid userId);
public Task<Result> SetNewIcon(Guid userId, Guid iconId); public Task<Result<Unit, Error>> SetNewIcon(Guid userId, Guid iconId);
} }
+6 -5
View File
@@ -2,6 +2,7 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Profiles; namespace Govor.Application.Profiles;
@@ -16,7 +17,7 @@ public class ProfileService : IProfileService
_logger = logger; _logger = logger;
} }
public async Task<Result<UserProfile>> GetUserProfileAsync(Guid userId) public async Task<Result<UserProfile, Error>> GetUserProfileAsync(Guid userId)
{ {
_logger.LogInformation("Getting user {UserId} profile", userId); _logger.LogInformation("Getting user {UserId} profile", userId);
@@ -34,13 +35,13 @@ public class ProfileService : IProfileService
if (profile is null) if (profile is null)
{ {
return Result<UserProfile>.Failure(CreateNotFoundError(userId)); return Result.Failure<UserProfile>(CreateNotFoundError(userId));
} }
return profile; return profile;
} }
public async Task<Result> SetDescription(string description, Guid userId) public async Task<Result<Unit, Error>> SetDescription(string description, Guid userId)
{ {
_logger.LogInformation("Updating description for user {UserId}", userId); _logger.LogInformation("Updating description for user {UserId}", userId);
@@ -56,7 +57,7 @@ public class ProfileService : IProfileService
return Result.Success(); return Result.Success();
} }
public async Task<Result> SetNewIcon(Guid userId, Guid iconId) public async Task<Result<Unit, Error>> SetNewIcon(Guid userId, Guid iconId)
{ {
_logger.LogInformation("Updating icon for user {UserId}", userId); _logger.LogInformation("Updating icon for user {UserId}", userId);
@@ -74,5 +75,5 @@ public class ProfileService : IProfileService
} }
private static Error CreateNotFoundError(Guid userId) => 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.");
} }
@@ -1,12 +1,13 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.PushNotifications; namespace Govor.Application.PushNotifications;
public interface IPushNotificationService public interface IPushNotificationService
{ {
Task<Result> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null); Task<Result<Unit, Error>> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null); Task<Result<Unit, Error>> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null); Task<Result<Unit, Error>> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
} }
@@ -1,14 +1,15 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.PushNotifications; namespace Govor.Application.PushNotifications;
public interface IPushTokenService public interface IPushTokenService
{ {
Task<Result> DeactivateTokenBySessionAsync(Guid sessionId); Task<Result<Unit, Error>> DeactivateTokenBySessionAsync(Guid sessionId);
Task<Result> DeactivateAllTokensByUserIdAsync(Guid userId); Task<Result<Unit, Error>> DeactivateAllTokensByUserIdAsync(Guid userId);
Task<Result<List<string>>> GetStringsActiveTokensAsync(Guid userId); Task<Result<List<string>, Error>> GetStringsActiveTokensAsync(Guid userId);
Task<Result<List<string>>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds); Task<Result<List<string>, Error>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds);
Task<Result<string?>> GetActiveTokenBySessionAsync(Guid sessionId); Task<Result<string?, Error>> GetActiveTokenBySessionAsync(Guid sessionId);
Task<Result> RemoveTokensAsync(IEnumerable<string> tokens); Task<Result<Unit, Error>> RemoveTokensAsync(IEnumerable<string> tokens);
Task<Result> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform); Task<Result<Unit, Error>> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform);
} }
@@ -2,6 +2,7 @@ using Govor.Application.Interfaces.PushNotifications.Models;
using Govor.Application.PushNotifications.Providers; using Govor.Application.PushNotifications.Providers;
using Govor.Domain.Common; using Govor.Domain.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.PushNotifications; namespace Govor.Application.PushNotifications;
@@ -21,7 +22,7 @@ public class PushNotificationService : IPushNotificationService
_logger = logger; _logger = logger;
} }
public async Task<Result> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null) public async Task<Result<Unit, Error>> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{ {
var resultTokens = await _tokenService.GetStringsActiveTokensAsync(userId); var resultTokens = await _tokenService.GetStringsActiveTokensAsync(userId);
if (resultTokens.IsFailure) if (resultTokens.IsFailure)
@@ -37,10 +38,10 @@ public class PushNotificationService : IPushNotificationService
return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, userId.ToString()); return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, userId.ToString());
} }
public async Task<Result> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null) public async Task<Result<Unit, Error>> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{ {
if (userIds == null || !userIds.Any()) 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); var tokensResult = await _tokenService.GetUsersStringsActiveTokensAsync(userIds);
if (tokensResult.IsFailure) if (tokensResult.IsFailure)
@@ -53,7 +54,7 @@ public class PushNotificationService : IPushNotificationService
return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, "bulk_request"); return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, "bulk_request");
} }
public async Task<Result> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null) public async Task<Result<Unit, Error>> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{ {
var tokenResult = await _tokenService.GetActiveTokenBySessionAsync(sessionId); var tokenResult = await _tokenService.GetActiveTokenBySessionAsync(sessionId);
if (tokenResult.IsFailure) if (tokenResult.IsFailure)
@@ -83,7 +84,7 @@ public class PushNotificationService : IPushNotificationService
} }
} }
private async Task<Result> SendMulticastInternalAsync(List<string> tokens, string title, string body, string channelId, string tag, Dictionary<string, string>? data, string targetInfo) private async Task<Result<Unit, Error>> SendMulticastInternalAsync(List<string> tokens, string title, string body, string channelId, string tag, Dictionary<string, string>? data, string targetInfo)
{ {
try try
{ {
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.PushNotifications; namespace Govor.Application.PushNotifications;
@@ -17,7 +18,7 @@ public class PushTokenService : IPushTokenService
_logger = logger; _logger = logger;
} }
public async Task<Result> DeactivateTokenBySessionAsync(Guid sessionId) public async Task<Result<Unit, Error>> DeactivateTokenBySessionAsync(Guid sessionId)
{ {
try try
{ {
@@ -34,7 +35,7 @@ public class PushTokenService : IPushTokenService
} }
} }
public async Task<Result> DeactivateAllTokensByUserIdAsync(Guid userId) public async Task<Result<Unit, Error>> DeactivateAllTokensByUserIdAsync(Guid userId)
{ {
try try
{ {
@@ -51,7 +52,7 @@ public class PushTokenService : IPushTokenService
} }
} }
public async Task<Result<List<string>>> GetStringsActiveTokensAsync(Guid userId) public async Task<Result<List<string>, Error>> GetStringsActiveTokensAsync(Guid userId)
{ {
try try
{ {
@@ -66,11 +67,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to fetch active push tokens for user {UserId}", userId); _logger.LogError(ex, "Failed to fetch active push tokens for user {UserId}", userId);
return Result<List<string>>.Failure(ex); return Result.Failure<List<string>>(ex);
} }
} }
public async Task<Result<List<string>>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds) public async Task<Result<List<string>, Error>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds)
{ {
try try
{ {
@@ -85,11 +86,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to fetch active push tokens for bulk users"); _logger.LogError(ex, "Failed to fetch active push tokens for bulk users");
return Result<List<string>>.Failure(ex); return Result.Failure<List<string>>(ex);
} }
} }
public async Task<Result<string?>> GetActiveTokenBySessionAsync(Guid sessionId) public async Task<Result<string?, Error>> GetActiveTokenBySessionAsync(Guid sessionId)
{ {
try try
{ {
@@ -104,11 +105,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to fetch active push token for session {SessionId}", sessionId); _logger.LogError(ex, "Failed to fetch active push token for session {SessionId}", sessionId);
return Result<string?>.Failure(ex); return Result.Failure<string?>(ex);
} }
} }
public async Task<Result> RemoveTokensAsync(IEnumerable<string> tokens) public async Task<Result<Unit, Error>> RemoveTokensAsync(IEnumerable<string> tokens)
{ {
if (tokens is null || !tokens.Any()) if (tokens is null || !tokens.Any())
return Result.Success(); return Result.Success();
@@ -128,11 +129,11 @@ public class PushTokenService : IPushTokenService
} }
} }
public async Task<Result> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform) public async Task<Result<Unit, Error>> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform)
{ {
if (string.IsNullOrWhiteSpace(token)) if (string.IsNullOrWhiteSpace(token))
{ {
return new Error("PushToken.Empty", "Push token cannot be empty."); return Result<Unit, Error>.Failure(Error.Failure("PushToken.Empty", "Push token cannot be empty."));
} }
var existingToken = await _context.UserPushTokens var existingToken = await _context.UserPushTokens
@@ -1,11 +1,12 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
public interface IUserSessionOpener public interface IUserSessionOpener
{ {
Task<Result<RefreshResult>> OpenSessionAsync(User user, string deviceInfo); Task<Result<RefreshResult, Error>> OpenSessionAsync(User user, string deviceInfo);
} }
@@ -1,8 +1,9 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
public interface IUserSessionReader public interface IUserSessionReader
{ {
Task<Result<List<Domain.Models.Users.UserSession>>> GetAllSessionsAsync(Guid userId); Task<Result<List<Domain.Models.Users.UserSession>, Error>> GetAllSessionsAsync(Guid userId);
} }
@@ -1,8 +1,9 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
public interface IUserSessionRefresher public interface IUserSessionRefresher
{ {
Task<Result<RefreshResult>> RefreshTokenAsync(string refreshToken); Task<Result<RefreshResult, Error>> RefreshTokenAsync(string refreshToken);
} }
@@ -1,10 +1,11 @@
using Govor.Domain.Common; using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
public interface IUserSessionRevoker public interface IUserSessionRevoker
{ {
Task<Result> CloseSessionByIdAsync(Guid sessionId, Guid userId); Task<Result<Unit, Error>> CloseSessionByIdAsync(Guid sessionId, Guid userId);
Task<Result> CloseAllSessionsAsync(Guid userId); Task<Result<Unit, Error>> CloseAllSessionsAsync(Guid userId);
} }
@@ -1,9 +1,10 @@
using Govor.Application.Authentication.JWT; using Govor.Application.Authentication.JWT;
using Govor.Domain; using Govor.Domain;
using Govor.Domain.Common; // Путь к вашему Result и Error using Govor.Domain.Common;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
@@ -32,7 +33,7 @@ public class UserSessionOpener : IUserSessionOpener
_jwtService = jwtService; _jwtService = jwtService;
} }
public async Task<Result<RefreshResult>> OpenSessionAsync(User user, string deviceInfo) public async Task<Result<RefreshResult, Error>> OpenSessionAsync(User user, string deviceInfo)
{ {
_logger.LogInformation("Opening session for user {UserId} on device '{DeviceInfo}'", user.Id, 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) if (result.IsFailure)
{ {
_logger.LogError("Failed to fetch sessions for user {UserId}: {Error}", user.Id, result.Error.Message); _logger.LogError("Failed to fetch sessions for user {UserId}: {Error}", user.Id, result.Error.Message);
return Result<RefreshResult>.Failure(result.Error); return Result.Failure<RefreshResult>(result.Error);
} }
var sessions = result.Value; var sessions = result.Value;
@@ -67,7 +68,7 @@ public class UserSessionOpener : IUserSessionOpener
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Database error while opening session for user {UserId}", user.Id); _logger.LogError(ex, "Database error while opening session for user {UserId}", user.Id);
return Result<RefreshResult>.Failure(ex); return Result.Failure<RefreshResult>(ex);
} }
} }
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models.Users; using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
@@ -17,13 +18,14 @@ public class UserSessionReader : IUserSessionReader
_logger = logger; _logger = logger;
} }
public async Task<Result<List<UserSession>>> GetAllSessionsAsync(Guid userId) public async Task<Result<List<UserSession>, Error>> GetAllSessionsAsync(Guid userId)
{ {
if (userId == Guid.Empty) if (userId == Guid.Empty)
{ {
return Result<List<UserSession>>.Failure(new Error( return Result.Failure<List<UserSession>>(Error.Conflict("UserSession.InvalidUserId",
"UserSession.InvalidUserId", "Provided User ID cannot be empty.")
"Provided User ID cannot be empty.")); );
} }
_logger.LogInformation("Getting all active sessions for user {UserId}", userId); _logger.LogInformation("Getting all active sessions for user {UserId}", userId);
@@ -40,7 +42,7 @@ public class UserSessionReader : IUserSessionReader
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to fetch user sessions for user {UserId}", userId); _logger.LogError(ex, "Failed to fetch user sessions for user {UserId}", userId);
return Result<List<UserSession>>.Failure(ex); return Result.Failure<List<UserSession>>(ex);
} }
} }
} }
@@ -4,6 +4,7 @@ using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
@@ -29,11 +30,11 @@ public class UserSessionRefresher : IUserSessionRefresher
_context = context; _context = context;
} }
public async Task<Result<RefreshResult>> RefreshTokenAsync(string refreshToken) public async Task<Result<RefreshResult, Error>> RefreshTokenAsync(string refreshToken)
{ {
if (string.IsNullOrWhiteSpace(refreshToken)) if (string.IsNullOrWhiteSpace(refreshToken))
{ {
return Result<RefreshResult>.Failure(new Error("Auth.EmptyToken", "Refresh token cannot be empty.")); return Result.Failure<RefreshResult>(Error.Failure("Auth.EmptyToken", "Refresh token cannot be empty."));
} }
var hashedToken = _jwtTokenHasher.HashToken(refreshToken); var hashedToken = _jwtTokenHasher.HashToken(refreshToken);
@@ -48,13 +49,13 @@ public class UserSessionRefresher : IUserSessionRefresher
if (session is null) if (session is null)
{ {
_logger.LogWarning("Refresh token session not found for hashed token"); _logger.LogWarning("Refresh token session not found for hashed token");
return Result<RefreshResult>.Failure(new Error("Auth.InvalidToken", "Invalid refresh token.")); return Result.Failure<RefreshResult>(Error.Failure("Auth.InvalidToken", "Invalid refresh token."));
} }
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow) if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
{ {
_logger.LogWarning("Attempted to refresh an expired or revoked session: {SessionId}", session.Id); _logger.LogWarning("Attempted to refresh an expired or revoked session: {SessionId}", session.Id);
return Result<RefreshResult>.Failure(new Error("Auth.InvalidToken", "Refresh token is invalid or expired.")); return Result.Failure<RefreshResult>(Error.Failure("Auth.InvalidToken", "Refresh token is invalid or expired."));
} }
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(session.User, session.Id); var newAccessToken = await _jwtService.GenerateAccessTokenAsync(session.User, session.Id);
@@ -74,7 +75,7 @@ public class UserSessionRefresher : IUserSessionRefresher
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Database error occurred during token refresh execution"); _logger.LogError(ex, "Database error occurred during token refresh execution");
return Result<RefreshResult>.Failure(ex); return Result.Failure<RefreshResult>(ex);
} }
} }
} }
@@ -3,6 +3,7 @@ using Govor.Domain;
using Govor.Domain.Common; using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Users.UserSessions; namespace Govor.Application.Users.UserSessions;
@@ -22,7 +23,7 @@ public class UserSessionRevoker : IUserSessionRevoker
_logger = logger; _logger = logger;
} }
public async Task<Result> CloseSessionByIdAsync(Guid sessionId, Guid userId) public async Task<Result<Unit, Error>> CloseSessionByIdAsync(Guid sessionId, Guid userId)
{ {
_logger.LogInformation("Attempting to close session {SessionId} for user {UserId}", sessionId, 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) if (session is null)
{ {
_logger.LogWarning("Active session {SessionId} not found or doesn't belong to user {UserId}", sessionId, userId); _logger.LogWarning("Active session {SessionId} not found or doesn't belong to user {UserId}", sessionId, userId);
return Result.Failure(new Error(
"UserSession.NotFoundOrUnauthorized", return Result.Failure(
$"Active session {sessionId} for user {userId} was not found.")); Error.NotFound(
"UserSession.NotFoundOrUnauthorized",
$"Active session {sessionId} for user {userId} was not found."
)
);
} }
session.IsRevoked = true; session.IsRevoked = true;
@@ -55,7 +60,7 @@ public class UserSessionRevoker : IUserSessionRevoker
} }
} }
public async Task<Result> CloseAllSessionsAsync(Guid userId) public async Task<Result<Unit, Error>> CloseAllSessionsAsync(Guid userId)
{ {
_logger.LogInformation("Attempting to close all active sessions for user {UserId}", userId); _logger.LogInformation("Attempting to close all active sessions for user {UserId}", userId);
@@ -25,8 +25,4 @@
<Using Include="NUnit.Framework" /> <Using Include="NUnit.Framework" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Govor.ConsoleClient\Govor.ConsoleClient.csproj" />
</ItemGroup>
</Project> </Project>
-28
View File
@@ -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);
}
}
}
@@ -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<ICommandDispatcher>();
var commands = dispatcher.GetAllCommands();
if (context.Arguments is not null)
{
var command = commands.FirstOrDefault(c =>
(c.GetType().GetCustomAttribute<CommandRouteAttribute>()?.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<CommandRouteAttribute>()?.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() => "Необходима для получения информации о доступных командах";
}
-17
View File
@@ -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;
}
@@ -1,7 +0,0 @@
namespace Govor.ConsoleClient.Commands;
public interface IInteractiveCommand : ICommand
{
Task HandleInputAsync(string input);
bool IsCompleted { get; }
}
@@ -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 "Отпарвка тестовых сообщений существующему юзеру";
}
}
@@ -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<CommandContext>();
// Middleware
services.AddSingleton<ICommandMiddleware, ExceptionHandlingMiddleware>();
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;
}
}
@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Api\" />
<Folder Include="Commands\Auth\" />
<Folder Include="Models\" />
<Folder Include="Utils\" />
</ItemGroup>
</Project>
-18
View File
@@ -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<App>(serviceProvider);
await app.RunAsync();
}
}
@@ -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;
}
}
@@ -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<IInputPipeline, InputPipeline>();
services.AddSingleton<ICommandDispatcher, CommandDispatcher>();
services.AddSingleton<IMiddlewarePipeline, MiddlewarePipeline>();
services.AddSingleton<ConsoleLogger>();
services.AddSingleton<ILogger>(sp => sp.GetRequiredService<ConsoleLogger>());
return services;
}
}
@@ -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<string, ICommand> _commands = new();
private readonly ILogger _logger;
private readonly IMiddlewarePipeline _pipeline;
public CommandDispatcher(IEnumerable<ICommand> commands, ILogger logger, IMiddlewarePipeline pipeline)
{
_logger = logger;
_pipeline = pipeline;
foreach (var command in commands)
{
var route = command.GetType().GetCustomAttribute<CommandRouteAttribute>()?.Path.Replace("/","")
?? command.GetType().Name.Replace("Command", "").ToLower();
_commands[route.ToLower()] = command;
}
}
public async Task<ICommand?> 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<ICommand> GetAllCommands() => _commands.Values;
}
@@ -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();
}
}
@@ -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, чтобы узнать доступные команды!");
}
}
}
}
@@ -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<Task> next);
public class MiddlewarePipeline : IMiddlewarePipeline
{
private readonly IList<ICommandMiddleware> _middlewares;
public MiddlewarePipeline(IEnumerable<ICommandMiddleware> 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);
}
}
@@ -1,9 +0,0 @@
using Govor.ConsoleClient.Commands;
namespace Govor.ConsoleClient.Services.Interfaces;
public interface ICommandDispatcher
{
Task<ICommand?> DispatchAsync(string input);
IEnumerable<ICommand> GetAllCommands();
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Interfaces;
public interface IInputPipeline
{
Task ProcessInputAsync(string input);
}
@@ -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);
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Interfaces;
public interface IMiddlewarePipeline
{
Task ExecuteAsync(CommandContext context);
}
@@ -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<Task> next)
{
try
{
await next();
}
catch (Exception ex)
{
_logger.Error($"Произошла ошибка при выполнении команды '{context?.Route}': {ex.Message}");
}
}
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Middleware;
public interface ICommandMiddleware
{
Task InvokeAsync(CommandContext context, Func<Task> next);
}
-3
View File
@@ -1,3 +0,0 @@
{
"BaseUrl": "https://govor-team-govor-88b3.twc1.net"
}
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
@@ -11,6 +11,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+11 -4
View File
@@ -1,9 +1,16 @@
namespace Govor.Domain.Common; namespace Govor.Domain.Common;
public record Error(string Code, string Message, ErrorType Type, Dictionary<string, string[]>? Errors = null)
public record Error(string Code, string Message)
{ {
public static readonly Error None = new(string.Empty, string.Empty); public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);
public static readonly Error Null = new("NULL", "Value cannot be null.");
public static Error Validation(string code, string message, Dictionary<string, string[]>? 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}"; public override string ToString() => $"{Code}: {Message}";
} }
+11
View File
@@ -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)
}
+14 -43
View File
@@ -1,49 +1,20 @@
using SmartRes;
namespace Govor.Domain.Common; namespace Govor.Domain.Common;
public class Result public static class Result
{ {
protected Result(bool isSuccess, Error error) // Для методов, возвращающих значение
{ public static Result<T, Error> Success<T>(T value) => Result<T, Error>.Success(value);
if (isSuccess && error != Error.None || !isSuccess && error == Error.None) public static Result<T, Error> Failure<T>(Error error) => Result<T, Error>.Failure(error);
{
throw new ArgumentException("Invalid error state", nameof(error));
}
IsSuccess = isSuccess; // Для методов, которые раньше возвращали просто Result (без T)
Error = error; public static Result<Unit, Error> Success() => Result<Unit, Error>.Success(Unit.Value);
} public static Result<Unit, Error> Failure(Error error) => Result<Unit, Error>.Failure(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);
public static Result Failure(Exception ex) => new(false, new Error(ex.GetType().Name, ex.Message)); public static Result<Unit, Error> Failure(Exception ex) =>
Result<Unit, Error>.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure));
public static implicit operator Result(Error error) => Failure(error);
} public static Result<T, Error> Failure<T>(Exception ex) =>
Result<T, Error>.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure));
public class Result<T> : 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<T> Success(T value) => new(value, true, Error.None);
public static new Result<T> Failure(Error error) => new(default, false, error);
public static new Result<T> Failure(Exception ex) => new(default, false, new Error(ex.GetType().Name, ex.Message));
public static implicit operator Result<T>(T value) => Success(value);
public static implicit operator Result<T>(Error error) => Failure(error);
public static implicit operator T(Result<T> result) => result.Value;
} }
+10 -4
View File
@@ -1,14 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" /> <PackageReference Include="Microting.EntityFrameworkCore.MySql" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>
+6 -7
View File
@@ -4,8 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API.Tests", "Govor.AP
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API", "Govor.API\Govor.API.csproj", "{EA8F272F-4276-438A-9DEA-C58860A440AE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API", "Govor.API\Govor.API.csproj", "{EA8F272F-4276-438A-9DEA-C58860A440AE}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{114F53C1-B0AB-4BA0-9E36-0E811D1B3776}" 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Domain", "Govor.Domain\Govor.Domain.csproj", "{868EAA88-A8E8-4508-B264-1AE3576D47C1}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Domain", "Govor.Domain\Govor.Domain.csproj", "{868EAA88-A8E8-4508-B264-1AE3576D47C1}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartRes", "..\SmartRes\SmartRes\SmartRes.csproj", "{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{EA8F272F-4276-438A-9DEA-C58860A440AE}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{4E94907F-BE20-42A6-AB15-637850FEAD11}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{868EAA88-A8E8-4508-B264-1AE3576D47C1}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{15031CBD-F319-4755-BA91-B86F20BD8E37} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4} {15031CBD-F319-4755-BA91-B86F20BD8E37} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}
{EA8F272F-4276-438A-9DEA-C58860A440AE} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776} {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} {4E94907F-BE20-42A6-AB15-637850FEAD11} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776}
{FC5EDCA8-FD58-4078-8FB1-2BDBB2F6CA3E} = {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} {F56A64DF-2938-4BE0-83F2-B86429F19259} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "8.0.0", "version": "10.0.0",
"rollForward": "latestMajor", "rollForward": "latestMajor",
"allowPrerelease": true "allowPrerelease": true
} }
BIN
View File
Binary file not shown.