diff --git a/.gitignore b/.gitignore index 18d592b..a093bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -470,7 +470,6 @@ FodyWeavers.xsd *.usertasks #Mono Project Files -*.resources test-results/ ### Windows template diff --git a/Dockerfile b/Dockerfile index a1b9168..753e470 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:6.0 +ARG APP_VERSION=0.0.0 + WORKDIR /app COPY ./publish /app/ @@ -9,6 +11,8 @@ RUN ["mkdir", "/app/data"] ENV DOTNET_RUNNING_IN_CONTAINER=true ENV ASPNETCORE_URLS=http://+:80 +ENV MAACOPILOT_APP_VERSION=$APP_VERSION + VOLUME ["/app/data"] EXPOSE 80/tcp diff --git a/MaaCopilotServer.sln b/MaaCopilotServer.sln index 9dc07bb..94a4d73 100644 --- a/MaaCopilotServer.sln +++ b/MaaCopilotServer.sln @@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaaCopilotServer.Applicatio EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{A1D2EB1C-F793-4CA9-86B6-8EB065CD75A7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resource", "Resource", "{649ECEA5-336F-4CF5-8D97-C9EA415A408B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaaCopilotServer.Resources", "src\MaaCopilotServer.Resources\MaaCopilotServer.Resources.csproj", "{0FC05A37-6FC9-49B6-9704-3488043D3A0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,11 +36,16 @@ Global {DB2CF416-793B-4546-B034-ADD57BDCD43B}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB2CF416-793B-4546-B034-ADD57BDCD43B}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB2CF416-793B-4546-B034-ADD57BDCD43B}.Release|Any CPU.Build.0 = Release|Any CPU + {0FC05A37-6FC9-49B6-9704-3488043D3A0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FC05A37-6FC9-49B6-9704-3488043D3A0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FC05A37-6FC9-49B6-9704-3488043D3A0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FC05A37-6FC9-49B6-9704-3488043D3A0D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5748A5AC-B6F8-4414-B6AA-A089407BC969} = {A1D2EB1C-F793-4CA9-86B6-8EB065CD75A7} {DB2CF416-793B-4546-B034-ADD57BDCD43B} = {A1D2EB1C-F793-4CA9-86B6-8EB065CD75A7} {2ED22FCD-5970-47F4-B5CB-3569F28788D3} = {A1D2EB1C-F793-4CA9-86B6-8EB065CD75A7} {EC4E807F-B785-4425-B695-B284A01DAA5E} = {A1D2EB1C-F793-4CA9-86B6-8EB065CD75A7} + {0FC05A37-6FC9-49B6-9704-3488043D3A0D} = {649ECEA5-336F-4CF5-8D97-C9EA415A408B} EndGlobalSection EndGlobal diff --git a/build-docker.sh b/build-docker.sh index 63a2d06..72689e1 100755 --- a/build-docker.sh +++ b/build-docker.sh @@ -1,7 +1,7 @@ if [ -z "$1" ]; then echo ">>>>> ERROR: Version could not be empty!" - exit + exit 1 fi -docker buildx build --load --platform linux/amd64 -t maa-copilot-server:"$1"-amd64 . -docker buildx build --load --platform linux/arm64 -t maa-copilot-server:"$1"-arm64 . +docker buildx build --load --build-arg APP_VERSION="$1" --platform linux/amd64 -t maa-copilot-server:"$1"-amd64 . +docker buildx build --load --build-arg APP_VERSION="$1" --platform linux/arm64 -t maa-copilot-server:"$1"-arm64 . diff --git a/src/MaaCopilotServer.Api/Helper/ConfigurationHelper.cs b/src/MaaCopilotServer.Api/Helper/ConfigurationHelper.cs index 428a933..a7ccdcf 100644 --- a/src/MaaCopilotServer.Api/Helper/ConfigurationHelper.cs +++ b/src/MaaCopilotServer.Api/Helper/ConfigurationHelper.cs @@ -4,7 +4,6 @@ using System.Reflection; using MaaCopilotServer.Application.Common.Extensions; -using ILogger = Serilog.ILogger; namespace MaaCopilotServer.Api.Helper; @@ -20,7 +19,7 @@ public static IConfiguration BuildConfiguration() var dataDirectoryEnv = Environment.GetEnvironmentVariable("MAA_DATA_DIRECTORY"); var isInDocker = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") ?? "false"; - var assemblyDirectory = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.NotNull(); + var assemblyDirectory = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.IsNotNull(); var dataDirectory = string.IsNullOrEmpty(dataDirectoryEnv) ? new DirectoryInfo(assemblyDirectory.FullName.CombinePath("data")).EnsureCreated() : new DirectoryInfo(dataDirectoryEnv).EnsureCreated(); @@ -61,10 +60,14 @@ public static IConfiguration BuildConfiguration() configurationBuilder.AddEnvironmentVariables("MAA_"); + var appVersion = Environment.GetEnvironmentVariable("MAACOPILOT_APP_VERSION") ?? "0.0.0"; + configurationBuilder.AddInMemoryCollection(new List> { new("Application:AssemblyPath", assemblyDirectory.FullName), - new("Application:DataDirectory", dataDirectory.FullName) + new("Application:DataDirectory", dataDirectory.FullName), + new("Application:Version", appVersion), + new("ElasticApm:ServiceVersion", appVersion) }); var configuration = configurationBuilder.Build(); diff --git a/src/MaaCopilotServer.Api/Helper/LoggerConfigurationHelper.cs b/src/MaaCopilotServer.Api/Helper/LoggerConfigurationHelper.cs new file mode 100644 index 0000000..51a8bf2 --- /dev/null +++ b/src/MaaCopilotServer.Api/Helper/LoggerConfigurationHelper.cs @@ -0,0 +1,58 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +using Destructurama; +using Elastic.Apm.SerilogEnricher; +using Elastic.CommonSchema.Serilog; +using Serilog; +using Serilog.Sinks.Elasticsearch; + +namespace MaaCopilotServer.Api.Helper; + +public static class LoggerConfigurationHelper +{ + public static LoggerConfiguration GetLoggerConfiguration(this IConfiguration configuration) + { + var loggerConfiguration = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Destructure.UsingAttributes(); + + if (configuration.GetValue("Switches:ElasticSearch") is false) + { + return loggerConfiguration; + } + + var elasticUris = configuration.GetValue("ElasticLogSink:Uris") + .Split(";").Select(x => new Uri(x)).ToArray(); + var elasticPeriod = configuration.GetValue("ElasticLogSink:Period"); + var elasticAuthMethod = configuration.GetValue("ElasticLogSink:Authentication:Method"); + var elasticId = configuration.GetValue("ElasticLogSink:Authentication:Secret:Id"); + var elasticKey = configuration.GetValue("ElasticLogSink:Authentication:Secret:Key"); + var elasticApplicationName = configuration.GetValue("ElasticLogSink:ApplicationName"); + loggerConfiguration.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(elasticUris) + { + Period = TimeSpan.FromSeconds(elasticPeriod), + AutoRegisterTemplate = true, + IndexFormat = $"{elasticApplicationName}-{DateTimeOffset.UtcNow.AddHours(8):yyyy.MM}", + CustomFormatter = new EcsTextFormatter(), + TypeName = null, + ModifyConnectionSettings = c => + { + switch (elasticAuthMethod) + { + case "Basic": + c.BasicAuthentication(elasticId, elasticKey); + break; + case "ApiKey": + c.ApiKeyAuthentication(elasticId, elasticKey); + break; + } + return c; + } + }); + loggerConfiguration.Enrich.WithElasticApmCorrelationInfo(); + + return loggerConfiguration; + } +} diff --git a/src/MaaCopilotServer.Api/MaaCopilotServer.Api.csproj b/src/MaaCopilotServer.Api/MaaCopilotServer.Api.csproj index af3421b..9d4334d 100644 --- a/src/MaaCopilotServer.Api/MaaCopilotServer.Api.csproj +++ b/src/MaaCopilotServer.Api/MaaCopilotServer.Api.csproj @@ -19,13 +19,16 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MaaCopilotServer.Api/Middleware/MiddlewareExtension.cs b/src/MaaCopilotServer.Api/Middleware/MiddlewareExtension.cs new file mode 100644 index 0000000..8d4622b --- /dev/null +++ b/src/MaaCopilotServer.Api/Middleware/MiddlewareExtension.cs @@ -0,0 +1,14 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +namespace MaaCopilotServer.Api.Middleware; + +public static class MiddlewareExtension +{ + public static IApplicationBuilder UseRequestCulture(this IApplicationBuilder app) + { + app.UseMiddleware(); + return app; + } +} diff --git a/src/MaaCopilotServer.Api/Middleware/RequestCultureMiddleware.cs b/src/MaaCopilotServer.Api/Middleware/RequestCultureMiddleware.cs new file mode 100644 index 0000000..a79eafc --- /dev/null +++ b/src/MaaCopilotServer.Api/Middleware/RequestCultureMiddleware.cs @@ -0,0 +1,29 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +using System.Globalization; +using MaaCopilotServer.Resources; + +namespace MaaCopilotServer.Api.Middleware; + +public class RequestCultureMiddleware +{ + private readonly RequestDelegate _next; + + public RequestCultureMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, ValidationErrorMessage validationErrorMessage, ApiErrorMessage apiErrorMessage) + { + var hasCulture = context.Request.Query.TryGetValue("culture", out var culture); + var info = hasCulture ? new CultureInfo(culture) : new CultureInfo("zh-cn"); + + validationErrorMessage.CultureInfo = info; + apiErrorMessage.CultureInfo = info; + + await _next(context); + } +} diff --git a/src/MaaCopilotServer.Api/Program.cs b/src/MaaCopilotServer.Api/Program.cs index 41181de..608048b 100644 --- a/src/MaaCopilotServer.Api/Program.cs +++ b/src/MaaCopilotServer.Api/Program.cs @@ -3,38 +3,23 @@ // Licensed under the AGPL-3.0 license. using Elastic.Apm.AspNetCore; +using Elastic.Apm.AspNetCore.DiagnosticListener; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Elasticsearch; using Elastic.Apm.EntityFrameworkCore; -using Elastic.CommonSchema.Serilog; using MaaCopilotServer.Api; using MaaCopilotServer.Api.Helper; +using MaaCopilotServer.Api.Middleware; using MaaCopilotServer.Application; using MaaCopilotServer.Infrastructure; +using MaaCopilotServer.Resources; using Serilog; -using Serilog.Sinks.Elasticsearch; +using Serilog.Debugging; var configuration = ConfigurationHelper.BuildConfiguration(); -var loggerConfiguration = new LoggerConfiguration() - .ReadFrom.Configuration(configuration); - -if (configuration.GetValue("Switches:ElasticSearch")) -{ - var elasticUris = configuration.GetValue("ElasticLogSink:Uris").Split(";").Select(x => new Uri(x)).ToArray(); - var elasticPeriod = configuration.GetValue("ElasticLogSink:Period"); - var elasticApiId = configuration.GetValue("ElasticLogSink:ApiId"); - var elasticApiKey = configuration.GetValue("ElasticLogSink:ApiKey"); - loggerConfiguration.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(elasticUris) - { - Period = TimeSpan.FromSeconds(elasticPeriod), - AutoRegisterTemplate = true, - IndexFormat = "maa-copilot-{0:yyyy.MM.dd}", - ModifyConnectionSettings = c => - c.ApiKeyAuthentication(elasticApiId, elasticApiKey), - CustomFormatter = new EcsTextFormatter() - }); -} - -Log.Logger = loggerConfiguration.CreateLogger(); +Log.Logger = configuration.GetLoggerConfiguration().CreateLogger(); +SelfLog.Enable(Console.Error); var builder = WebApplication.CreateBuilder(); @@ -43,6 +28,7 @@ builder.Configuration.AddConfiguration(configuration); builder.Services.AddControllers(); +builder.Services.AddResources(); builder.Services.AddInfrastructureServices(); builder.Services.AddApplicationServices(); builder.Services.AddApiServices(configuration); @@ -53,9 +39,15 @@ if (configuration.GetValue("Switches:Apm")) { - app.UseElasticApm(configuration, new EfCoreDiagnosticsSubscriber()); + app.UseElasticApm(configuration, + new EfCoreDiagnosticsSubscriber(), + new ElasticsearchDiagnosticsSubscriber(), + new HttpDiagnosticsSubscriber(), + new AspNetCoreDiagnosticSubscriber(), + new AspNetCoreErrorDiagnosticsSubscriber()); } +app.UseRequestCulture(); app.UseAuthentication(); app.MapControllers(); diff --git a/src/MaaCopilotServer.Api/appsettings.ElasticSearch.json b/src/MaaCopilotServer.Api/appsettings.ElasticSearch.json new file mode 100644 index 0000000..063c41c --- /dev/null +++ b/src/MaaCopilotServer.Api/appsettings.ElasticSearch.json @@ -0,0 +1,69 @@ +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {ElasticApmTraceId} {ElasticApmTransactionId} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {ElasticApmTraceId} {ElasticApmTransactionId} {Level:u3}] {ThreadId} {Message:lj}{NewLine}{Exception}", + "path": "{{ DATA DIRECTORY }}/logs/log-.log", + "rollingInterval": "Day", + "shared": true + } + } + ], + "Enrich": [ + "FromLogContext", + "WithThreadId" + ] + }, + "Jwt": { + "Secret": "YOUR STRONG 32+ BIT LENGTH (RECOMMEND 128 BIT OR MORE) SECRET TOKEN", + "Issuer": "MaaCopilot", + "Audience": "Doctor", + "ExpireTime": 720 + }, + "Database": { + "ConnectionString": "Server=127.0.0.1;Port=5432;Database=maa_copilot;User Id=maa_admin;Password=m@@_@dmin_p@ss;" + }, + "Switches": { + "ElasticSearch": true, + "Apm": true + }, + "ElasticLogSink": { + "ApplicationName": "maa-copilot", + "Uris": "http://localhost:9200", + "Period": 10, + "Authentication": { + "Method": "Basic", + "Secret": { + "Id": "", + "Key": "" + } + } + }, + "ElasticApm": { + "SecretToken": "", + "ServerUrl": "http://localhost:8200", + "ServiceName": "MaaCopilotServer", + "Environment": "production" + }, + "AllowedHosts": "*" +} diff --git a/src/MaaCopilotServer.Api/appsettings.json b/src/MaaCopilotServer.Api/appsettings.json index 491d0f5..0ebcb57 100644 --- a/src/MaaCopilotServer.Api/appsettings.json +++ b/src/MaaCopilotServer.Api/appsettings.json @@ -31,7 +31,6 @@ ], "Enrich": [ "FromLogContext", - "WithMachineName", "WithThreadId" ] }, @@ -49,10 +48,16 @@ "Apm": false }, "ElasticLogSink": { + "ApplicationName": "maa-copilot", "Uris": "http://localhost:9200", "Period": 10, - "ApiId": "", - "ApiKey": "" + "Authentication": { + "Method": "Basic", + "Secret": { + "Id": "", + "Key": "" + } + } }, "ElasticApm": { "SecretToken": "", diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/AuthorizationBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/AuthorizationBehaviour.cs index 67b6c7b..0f129ef 100644 --- a/src/MaaCopilotServer.Application/Common/Behaviours/AuthorizationBehaviour.cs +++ b/src/MaaCopilotServer.Application/Common/Behaviours/AuthorizationBehaviour.cs @@ -3,27 +3,28 @@ // Licensed under the AGPL-3.0 license. using System.Reflection; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; -using MediatR; namespace MaaCopilotServer.Application.Common.Behaviours; public class AuthorizationBehaviour : IPipelineBehavior where TRequest : IRequest { - private readonly IIdentityService _identityService; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IIdentityService _identityService; - public AuthorizationBehaviour(IIdentityService identityService, ICurrentUserService currentUserService) + public AuthorizationBehaviour( + IIdentityService identityService, + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _identityService = identityService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) { var authorizeAttributes = request.GetType().GetCustomAttributes().ToList(); if (authorizeAttributes.Count == 0) @@ -34,19 +35,20 @@ public async Task Handle(TRequest request, CancellationToken cancella var userId = _currentUserService.GetUserIdentity(); if (userId is null) { - throw new PipelineException(MaaApiResponse.Unauthorized(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.Unauthorized(_currentUserService.GetTrackingId(), _apiErrorMessage.Unauthorized)); } var user = await _identityService.GetUserAsync(userId.Value); if (user is null) { - throw new PipelineException(MaaApiResponse.NotFound($"User {userId}", _currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.UserWithIdNotFound!, userId))); } var roleRequired = authorizeAttributes.First().Role; if (user.UserRole < roleRequired) { - throw new PipelineException(MaaApiResponse.Forbidden(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.Forbidden(_currentUserService.GetTrackingId(), _apiErrorMessage.PermissionDenied)); } return await next(); diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/LoggingBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/LoggingBehaviour.cs index 3d98bdd..73f5399 100644 --- a/src/MaaCopilotServer.Application/Common/Behaviours/LoggingBehaviour.cs +++ b/src/MaaCopilotServer.Application/Common/Behaviours/LoggingBehaviour.cs @@ -2,20 +2,14 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.CopilotUser.Commands.ChangeCopilotUserInfo; -using MaaCopilotServer.Application.CopilotUser.Commands.CreateCopilotUser; -using MaaCopilotServer.Application.CopilotUser.Commands.LoginCopilotUser; -using MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserPassword; -using MediatR.Pipeline; using Microsoft.Extensions.Logging; namespace MaaCopilotServer.Application.Common.Behaviours; public class LoggingBehaviour : IRequestPreProcessor where TRequest : notnull { - private readonly ILogger _logger; private readonly ICurrentUserService _currentUserService; + private readonly ILogger _logger; // ReSharper disable once ContextualLoggerProblem public LoggingBehaviour(ILogger logger, ICurrentUserService currentUserService) @@ -28,17 +22,9 @@ public Task Process(TRequest request, CancellationToken cancellationToken) { var requestName = typeof(TRequest).Name; var userId = _currentUserService.GetUserIdentity()?.ToString() ?? "Anonymous"; - if (typeof(TRequest) == typeof(LoginCopilotUserCommand) || - typeof(TRequest) == typeof(CreateCopilotUserCommand) || - typeof(TRequest) == typeof(ChangeCopilotUserInfoCommand) || - typeof(TRequest) == typeof(UpdateCopilotUserPasswordCommand)) - { - _logger.LogInformation("MaaCopilotServer Request: {Name} {UserId} {@Request}", requestName, userId, "********"); - } - else - { - _logger.LogInformation("MaaCopilotServer Request: {Name} {UserId} {@Request}", requestName, userId, request); - } + _logger.LogInformation( + "MaaCopilotServer: Type -> {LoggingType}; Request Name -> {Name}; User -> {UserId}; Request -> {@Request}", + (string)LoggingType.Request, requestName, userId, request); return Task.CompletedTask; } } diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/PerformanceBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/PerformanceBehaviour.cs index d76d96d..a8efd2e 100644 --- a/src/MaaCopilotServer.Application/Common/Behaviours/PerformanceBehaviour.cs +++ b/src/MaaCopilotServer.Application/Common/Behaviours/PerformanceBehaviour.cs @@ -3,18 +3,17 @@ // Licensed under the AGPL-3.0 license. using System.Diagnostics; -using MaaCopilotServer.Application.Common.Interfaces; -using MediatR; using Microsoft.Extensions.Logging; namespace MaaCopilotServer.Application.Common.Behaviours; -public class PerformanceBehaviour : IPipelineBehavior where TRequest : IRequest +public class PerformanceBehaviour : IPipelineBehavior + where TRequest : IRequest { - private readonly Stopwatch _timer; - private readonly ILogger _logger; private readonly ICurrentUserService _currentUserService; private readonly IIdentityService _identityService; + private readonly ILogger _logger; + private readonly Stopwatch _timer; public PerformanceBehaviour( // ReSharper disable once ContextualLoggerProblem @@ -29,7 +28,8 @@ public PerformanceBehaviour( _identityService = identityService; } - public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) { _timer.Start(); @@ -48,8 +48,8 @@ public async Task Handle(TRequest request, CancellationToken cancella var userId = _currentUserService.GetUserIdentity().ToString() ?? string.Empty; _logger.LogWarning( - "MaaCopilotServer Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}", - requestName, elapsedMilliseconds, userId, request); + "MaaCopilotServer: Type -> {LoggingType}; Request Name -> {Name}; User -> {UserId}; Request -> {@Request}", + (string)LoggingType.LongRunRequest, requestName, userId, request); return response; } diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/PipelineExceptionBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/PipelineExceptionBehaviour.cs new file mode 100644 index 0000000..888a1cc --- /dev/null +++ b/src/MaaCopilotServer.Application/Common/Behaviours/PipelineExceptionBehaviour.cs @@ -0,0 +1,38 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +using Microsoft.Extensions.Logging; + +namespace MaaCopilotServer.Application.Common.Behaviours; + +public class PipelineExceptionBehaviour : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger _logger; + + // ReSharper disable once ContextualLoggerProblem + public PipelineExceptionBehaviour(ILogger logger) + { + _logger = logger; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + try + { + return await next(); + } + catch (PipelineException ex) + { + var requestName = typeof(TRequest).Name; + + _logger.LogError( + "MaaCopilotServer: Type -> {LoggingType}; Status Code -> {StatusCode}; Request Name -> {Name}; Request -> {@Request}", + (string)LoggingType.FailedRequest, ex.Result.RealStatusCode, requestName, request); + + throw; + } + } +} diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs index c9df28b..23e8ae4 100644 --- a/src/MaaCopilotServer.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs +++ b/src/MaaCopilotServer.Application/Common/Behaviours/UnhandledExceptionBehaviour.cs @@ -2,8 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Exceptions; -using MediatR; using Microsoft.Extensions.Logging; namespace MaaCopilotServer.Application.Common.Behaviours; @@ -12,11 +10,18 @@ public class UnhandledExceptionBehaviour : IPipelineBehavio where TRequest : IRequest { private readonly ILogger _logger; + private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; // ReSharper disable once ContextualLoggerProblem - public UnhandledExceptionBehaviour(ILogger logger) + public UnhandledExceptionBehaviour( + ILogger logger, + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _logger = logger; + _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) @@ -25,19 +30,18 @@ public async Task Handle(TRequest request, CancellationToken cancella { return await next(); } - catch (PipelineException ex) + catch (PipelineException) { - var requestName = typeof(TRequest).Name; - _logger.LogError(ex, - "MaaCopilotServer Request Pipeline Exception: {StatusCode} for Request {Name} {@Request}", ex.Result.RealStatusCode ,requestName, - request); throw; } catch (Exception ex) { var requestName = typeof(TRequest).Name; - _logger.LogError(ex, "MaaCopilotServer Request Exception: Unhandled Exception for Request {Name} {@Request}", requestName, request); - throw; + _logger.LogError(exception: ex, + "MaaCopilotServer: Type -> {LoggingType}; Request Name -> {Name}; Request -> {@Request};", + (string)LoggingType.Exception, requestName, request); + + throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId(), _apiErrorMessage.InternalException)); } } } diff --git a/src/MaaCopilotServer.Application/Common/Behaviours/ValidationBehaviour.cs b/src/MaaCopilotServer.Application/Common/Behaviours/ValidationBehaviour.cs index bd29a08..77ff58a 100644 --- a/src/MaaCopilotServer.Application/Common/Behaviours/ValidationBehaviour.cs +++ b/src/MaaCopilotServer.Application/Common/Behaviours/ValidationBehaviour.cs @@ -2,19 +2,13 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; - namespace MaaCopilotServer.Application.Common.Behaviours; public class ValidationBehaviour : IPipelineBehavior where TRequest : IRequest { - private readonly IEnumerable> _validators; private readonly ICurrentUserService _currentUserService; + private readonly IEnumerable> _validators; public ValidationBehaviour(IEnumerable> validators, ICurrentUserService currentUserService) { diff --git a/src/MaaCopilotServer.Application/Common/Enum/LoggingType.cs b/src/MaaCopilotServer.Application/Common/Enum/LoggingType.cs new file mode 100644 index 0000000..2fe0997 --- /dev/null +++ b/src/MaaCopilotServer.Application/Common/Enum/LoggingType.cs @@ -0,0 +1,25 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +namespace MaaCopilotServer.Application.Common.Enum; + +public struct LoggingType +{ + private readonly string _value; + + private LoggingType(string value) + { + _value = value; + } + + public static LoggingType Request => new("Request"); + public static LoggingType LongRunRequest => new("Long Run Request"); + public static LoggingType FailedRequest => new("Failed Request"); + public static LoggingType Exception => new("Exception"); + + public static implicit operator string(LoggingType loggingType) + { + return loggingType._value; + } +} diff --git a/src/MaaCopilotServer.Application/Common/Exceptions/FileFoundException.cs b/src/MaaCopilotServer.Application/Common/Exceptions/FileFoundException.cs index a42f77a..e06fdae 100644 --- a/src/MaaCopilotServer.Application/Common/Exceptions/FileFoundException.cs +++ b/src/MaaCopilotServer.Application/Common/Exceptions/FileFoundException.cs @@ -6,8 +6,8 @@ namespace MaaCopilotServer.Application.Common.Exceptions; public class FileFoundException : IOException { - private readonly string _message; private readonly string _fileName; + private readonly string _message; public FileFoundException(string message, string fileName) { diff --git a/src/MaaCopilotServer.Application/Common/Exceptions/PipelineException.cs b/src/MaaCopilotServer.Application/Common/Exceptions/PipelineException.cs index 81e100d..e68fdc8 100644 --- a/src/MaaCopilotServer.Application/Common/Exceptions/PipelineException.cs +++ b/src/MaaCopilotServer.Application/Common/Exceptions/PipelineException.cs @@ -2,8 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Models; - namespace MaaCopilotServer.Application.Common.Exceptions; public class PipelineException : Exception diff --git a/src/MaaCopilotServer.Application/Common/Extensions/DateTimeOffsetExtension.cs b/src/MaaCopilotServer.Application/Common/Extensions/DateTimeOffsetExtension.cs index c66f7a2..b7f3e42 100644 --- a/src/MaaCopilotServer.Application/Common/Extensions/DateTimeOffsetExtension.cs +++ b/src/MaaCopilotServer.Application/Common/Extensions/DateTimeOffsetExtension.cs @@ -10,7 +10,8 @@ public static class DateTimeOffsetExtension { public static string? ToStringZhHans(this DateTimeOffset? dateTimeOffset) { - if (dateTimeOffset is null) { return null;} + if (dateTimeOffset is null) { return null; } + var culture = new CultureInfo("zh-Hans"); return dateTimeOffset.Value.ToString("o", culture); } diff --git a/src/MaaCopilotServer.Application/Common/Extensions/DirectoryInfoExtension.cs b/src/MaaCopilotServer.Application/Common/Extensions/DirectoryInfoExtension.cs index 538801c..e945c2f 100644 --- a/src/MaaCopilotServer.Application/Common/Extensions/DirectoryInfoExtension.cs +++ b/src/MaaCopilotServer.Application/Common/Extensions/DirectoryInfoExtension.cs @@ -9,17 +9,22 @@ namespace MaaCopilotServer.Application.Common.Extensions; public static class DirectoryInfoExtension { /// - /// 确保目录存在,若不存在,则会创建 + /// 确保目录存在,若不存在,则会创建 /// - /// 类实例 - /// - /// - /// 类实例,并确保目录存在 + /// 类实例 + /// + /// + /// + /// + /// + /// + /// 类实例,并确保目录存在 public static DirectoryInfo EnsureCreated(this DirectoryInfo? directoryInfo, - [CallerArgumentExpression("directoryInfo")] string paramName = "UnknownParamName", + [CallerArgumentExpression("directoryInfo")] + string paramName = "UnknownParamName", [CallerMemberName] string memberName = "UnknownMemberName") { - var di = directoryInfo.NotNull(paramName: paramName, memberName: memberName); + var di = directoryInfo.IsNotNull(paramName, memberName); if (di.Exists is false) { di.Create(); diff --git a/src/MaaCopilotServer.Application/Common/Extensions/FileInfoExtension.cs b/src/MaaCopilotServer.Application/Common/Extensions/FileInfoExtension.cs index 7ea5061..10992d4 100644 --- a/src/MaaCopilotServer.Application/Common/Extensions/FileInfoExtension.cs +++ b/src/MaaCopilotServer.Application/Common/Extensions/FileInfoExtension.cs @@ -4,25 +4,28 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; -using MaaCopilotServer.Application.Common.Exceptions; namespace MaaCopilotServer.Application.Common.Extensions; public static class FileInfoExtension { /// - /// 确认文件存在,若文件不存在,则抛出 异常 + /// 确认文件存在,若文件不存在,则抛出 异常 /// - /// 实例 - /// - /// - /// 确保文件存在的 实例 + /// 实例 + /// + /// + /// + /// + /// + /// + /// 确保文件存在的 实例 /// 文件不存在异常 public static FileInfo AssertExist(this FileInfo? fileInfo, [CallerArgumentExpression("fileInfo")] string paramName = "UnknownParamName", [CallerMemberName] string memberName = "UnknownMemberName") { - var fi = fileInfo.NotNull(paramName, memberName); + var fi = fileInfo.IsNotNull(paramName, memberName); if (fi.Exists is false) { throw new FileNotFoundException($"从 {memberName} 请求确认的 FileInfo {paramName},文件不存在", fi.FullName); @@ -32,18 +35,22 @@ public static FileInfo AssertExist(this FileInfo? fileInfo, } /// - /// 确认文件不存在,若文件存在,则抛出 异常 + /// 确认文件不存在,若文件存在,则抛出 异常 /// - /// 实例 - /// - /// - /// 确保文件不存在的 实例 + /// 实例 + /// + /// + /// + /// + /// + /// + /// 确保文件不存在的 实例 /// 文件存在异常 public static FileInfo AssertNotExist(this FileInfo? fileInfo, [CallerArgumentExpression("fileInfo")] string paramName = "UnknownParamName", [CallerMemberName] string memberName = "UnknownMemberName") { - var fi = fileInfo.NotNull(paramName, memberName); + var fi = fileInfo.IsNotNull(paramName, memberName); if (fi.Exists is false) { throw new FileFoundException($"从 {memberName} 请求确认的 FileInfo {paramName},文件存在", fi.FullName); @@ -53,10 +60,10 @@ public static FileInfo AssertNotExist(this FileInfo? fileInfo, } /// - /// 确保文件不存在 + /// 确保文件不存在 /// - /// 实例 - /// 确保文件不存在的 实例 + /// 实例 + /// 确保文件不存在的 实例 public static FileInfo EnsureDeleted(this FileInfo fileInfo) { if (fileInfo.Exists) @@ -68,9 +75,9 @@ public static FileInfo EnsureDeleted(this FileInfo fileInfo) } /// - /// 获取文件 MD5 校验码 + /// 获取文件 MD5 校验码 /// - /// 实例 + /// 实例 /// 文件 MD5 校验码 public static string GetMd5(this FileInfo fileInfo) { @@ -85,9 +92,9 @@ public static string GetMd5(this FileInfo fileInfo) } /// - /// 异步获取文件 MD5 校验码 + /// 异步获取文件 MD5 校验码 /// - /// 实例 + /// 实例 /// 文件 MD5 校验码 public static async Task GetMd5Async(this FileInfo fileInfo) { @@ -102,10 +109,10 @@ public static async Task GetMd5Async(this FileInfo fileInfo) } /// - /// 检查两个文件的 MD5 校验码是否一致 + /// 检查两个文件的 MD5 校验码是否一致 /// - /// 实例 - /// 实例 + /// 实例 + /// 实例 /// 文件是否一致 public static bool IsSameMd5With(this FileInfo fileInfo, FileInfo another) { diff --git a/src/MaaCopilotServer.Application/Common/Extensions/NullableExtension.cs b/src/MaaCopilotServer.Application/Common/Extensions/NullableExtension.cs index 8706687..3a07986 100644 --- a/src/MaaCopilotServer.Application/Common/Extensions/NullableExtension.cs +++ b/src/MaaCopilotServer.Application/Common/Extensions/NullableExtension.cs @@ -9,15 +9,19 @@ namespace MaaCopilotServer.Application.Common.Extensions; public static class NullableExtension { /// - /// 检查对象不为 Null,否则抛出 异常 + /// 检查对象不为 Null,否则抛出 异常 /// /// 对象 - /// - /// + /// + /// + /// + /// + /// + /// /// 对象类型 /// 确保不为 Null 的对象 /// 对象为 Null - public static T NotNull(this T? obj, + public static T IsNotNull(this T? obj, [CallerArgumentExpression("obj")] string paramName = "UnknownParamName", [CallerMemberName] string memberName = "UnknownMemberName") { diff --git a/src/MaaCopilotServer.Application/Common/Extensions/PathExtension.cs b/src/MaaCopilotServer.Application/Common/Extensions/PathExtension.cs index 393a9d2..2d2bb5e 100644 --- a/src/MaaCopilotServer.Application/Common/Extensions/PathExtension.cs +++ b/src/MaaCopilotServer.Application/Common/Extensions/PathExtension.cs @@ -7,7 +7,7 @@ namespace MaaCopilotServer.Application.Common.Extensions; public static class PathExtension { /// - /// 拼接路径 + /// 拼接路径 /// /// 路径 1 /// 路径 2 diff --git a/src/MaaCopilotServer.Application/Common/Interfaces/IIdentityService.cs b/src/MaaCopilotServer.Application/Common/Interfaces/IIdentityService.cs index d66fd22..69c96a5 100644 --- a/src/MaaCopilotServer.Application/Common/Interfaces/IIdentityService.cs +++ b/src/MaaCopilotServer.Application/Common/Interfaces/IIdentityService.cs @@ -2,8 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Domain.Entities; - namespace MaaCopilotServer.Application.Common.Interfaces; public interface IIdentityService diff --git a/src/MaaCopilotServer.Application/Common/Models/EmptyObject.cs b/src/MaaCopilotServer.Application/Common/Models/EmptyObject.cs index 2f42b62..dd214a6 100644 --- a/src/MaaCopilotServer.Application/Common/Models/EmptyObject.cs +++ b/src/MaaCopilotServer.Application/Common/Models/EmptyObject.cs @@ -4,4 +4,6 @@ namespace MaaCopilotServer.Application.Common.Models; -public class EmptyObject { } +public class EmptyObject +{ +} diff --git a/src/MaaCopilotServer.Application/Common/Models/MaaActionResult.cs b/src/MaaCopilotServer.Application/Common/Models/MaaActionResult.cs index ebe2e10..ea89d7b 100644 --- a/src/MaaCopilotServer.Application/Common/Models/MaaActionResult.cs +++ b/src/MaaCopilotServer.Application/Common/Models/MaaActionResult.cs @@ -9,12 +9,14 @@ namespace MaaCopilotServer.Application.Common.Models; public class MaaActionResult { private readonly MaaApiResponse _maaApiResponse; - public int RealStatusCode => _maaApiResponse.StatusCode; + private MaaActionResult(MaaApiResponse maaApiResponse) { _maaApiResponse = maaApiResponse; } + public int RealStatusCode => _maaApiResponse.StatusCode; + public static implicit operator OkObjectResult(MaaActionResult result) { return new OkObjectResult(result._maaApiResponse); diff --git a/src/MaaCopilotServer.Application/Common/Models/MaaApiResponse.cs b/src/MaaCopilotServer.Application/Common/Models/MaaApiResponse.cs index b94aa2a..d56dc65 100644 --- a/src/MaaCopilotServer.Application/Common/Models/MaaApiResponse.cs +++ b/src/MaaCopilotServer.Application/Common/Models/MaaApiResponse.cs @@ -16,6 +16,11 @@ private MaaApiResponse(int statusCode, string message, string traceId, object? d Data = data; } + [JsonPropertyName("status_code")] public int StatusCode { get; } + [JsonPropertyName("message")] public string Message { get; } + [JsonPropertyName("trace_id")] public string TraceId { get; } + [JsonPropertyName("data")] public object? Data { get; } + public static MaaApiResponse Ok(object? obj, string id) { return new MaaApiResponse(200, "OK", id, obj); @@ -36,18 +41,13 @@ public static MaaApiResponse BadRequest(string id, string? message = null) return new MaaApiResponse(400, message ?? "Bad Request", id, null); } - public static MaaApiResponse NotFound(string resourceName, string id) + public static MaaApiResponse NotFound(string id, string? message = null) { - return new MaaApiResponse(404, $"{resourceName} Not Found", id, null); + return new MaaApiResponse(404, message ?? "Not Found", id, null); } - public static MaaApiResponse InternalError(string id) + public static MaaApiResponse InternalError(string id, string? message = null) { - return new MaaApiResponse(500, "Internal Server Error", id, null); + return new MaaApiResponse(500, message ?? "Internal Server Error", id, null); } - - [JsonPropertyName("status_code")] public int StatusCode { get; } - [JsonPropertyName("message")] public string Message { get; } - [JsonPropertyName("trace_id")] public string TraceId { get; } - [JsonPropertyName("data")] public object? Data { get; } } diff --git a/src/MaaCopilotServer.Application/Common/Models/PaginationResult.cs b/src/MaaCopilotServer.Application/Common/Models/PaginationResult.cs index 10e997b..5407a97 100644 --- a/src/MaaCopilotServer.Application/Common/Models/PaginationResult.cs +++ b/src/MaaCopilotServer.Application/Common/Models/PaginationResult.cs @@ -16,12 +16,11 @@ public PaginationResult(bool hasNext, int page, int total, List data) Data = data; } - [JsonPropertyName("has_next")] - public bool HasNext { get; set; } - [JsonPropertyName("page")] - public int Page { get; set; } - [JsonPropertyName("total")] - public int Total { get; set; } - [JsonPropertyName("data")] - public List Data { get; set; } + [JsonPropertyName("has_next")] public bool HasNext { get; set; } + + [JsonPropertyName("page")] public int Page { get; set; } + + [JsonPropertyName("total")] public int Total { get; set; } + + [JsonPropertyName("data")] public List Data { get; set; } } diff --git a/src/MaaCopilotServer.Application/Common/Security/AuthorizedAttribute.cs b/src/MaaCopilotServer.Application/Common/Security/AuthorizedAttribute.cs index 1250eae..49cc899 100644 --- a/src/MaaCopilotServer.Application/Common/Security/AuthorizedAttribute.cs +++ b/src/MaaCopilotServer.Application/Common/Security/AuthorizedAttribute.cs @@ -9,10 +9,10 @@ namespace MaaCopilotServer.Application.Common.Security; [AttributeUsage(AttributeTargets.Class)] public class AuthorizedAttribute : Attribute { - public UserRole Role { get; } - public AuthorizedAttribute(UserRole role) { Role = role; } + + public UserRole Role { get; } } diff --git a/src/MaaCopilotServer.Application/ConfigureServices.cs b/src/MaaCopilotServer.Application/ConfigureServices.cs index f514679..a88ff31 100644 --- a/src/MaaCopilotServer.Application/ConfigureServices.cs +++ b/src/MaaCopilotServer.Application/ConfigureServices.cs @@ -3,9 +3,6 @@ // Licensed under the AGPL-3.0 license. using System.Reflection; -using FluentValidation; -using MaaCopilotServer.Application.Common.Behaviours; -using MediatR; using Microsoft.Extensions.DependencyInjection; namespace MaaCopilotServer.Application; @@ -16,6 +13,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection { services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PipelineExceptionBehaviour<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommand.cs b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommand.cs index 7cde976..593c14a 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommand.cs @@ -4,11 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; using MaaCopilotServer.Domain.Enums; -using MediatR; namespace MaaCopilotServer.Application.CopilotOperation.Commands.CreateCopilotOperation; @@ -18,12 +14,13 @@ public record CreateCopilotOperationCommand : IRequest> +public class CreateCopilotOperationCommandHandler : IRequestHandler> { + private readonly ICopilotIdService _copilotIdService; + private readonly ICurrentUserService _currentUserService; private readonly IMaaCopilotDbContext _dbContext; private readonly IIdentityService _identityService; - private readonly ICurrentUserService _currentUserService; - private readonly ICopilotIdService _copilotIdService; public CreateCopilotOperationCommandHandler( IMaaCopilotDbContext dbContext, @@ -37,7 +34,8 @@ public CreateCopilotOperationCommandHandler( _copilotIdService = copilotIdService; } - public async Task> Handle(CreateCopilotOperationCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateCopilotOperationCommand request, + CancellationToken cancellationToken) { var doc = JsonDocument.Parse(request.Content!).RootElement; @@ -55,6 +53,7 @@ public async Task> Handle(CreateCopil { docTitle = titleElement.GetString() ?? string.Empty; } + if (docDetailsElementExist) { docDetails = detailsElement.GetString() ?? string.Empty; diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommandValidator.cs index 689744a..6041e95 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationCommandValidator.cs @@ -3,18 +3,17 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json; -using FluentValidation; namespace MaaCopilotServer.Application.CopilotOperation.Commands.CreateCopilotOperation; public class CreateCopilotOperationCommandValidator : AbstractValidator { - public CreateCopilotOperationCommandValidator() + public CreateCopilotOperationCommandValidator(ValidationErrorMessage errorMessage) { RuleFor(x => x.Content) - .NotNull() .NotEmpty() - .Must(BeValidatedContent); + .Must(BeValidatedContent) + .WithMessage(errorMessage.CopilotOperationJsonIsInvalid); } private static bool BeValidatedContent(string? content) diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationDto.cs b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationDto.cs index e0b35ec..21e9275 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationDto.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Commands/CreateCopilotOperation/CreateCopilotOperationDto.cs @@ -13,6 +13,5 @@ public CreateCopilotOperationDto(string id) Id = id; } - [JsonPropertyName("id")] - public string Id { get; set; } + [JsonPropertyName("id")] public string Id { get; set; } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommand.cs b/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommand.cs index d6b9109..201d627 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommand.cs @@ -3,11 +3,7 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotOperation.Commands.DeleteCopilotOperation; @@ -21,37 +17,43 @@ public record DeleteCopilotOperationCommand : IRequest> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICopilotIdService _copilotIdService; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public DeleteCopilotOperationCommandHandler( IMaaCopilotDbContext dbContext, ICopilotIdService copilotIdService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _copilotIdService = copilotIdService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(DeleteCopilotOperationCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteCopilotOperationCommand request, + CancellationToken cancellationToken) { var id = _copilotIdService.DecodeId(request.Id!); if (id is null) { - return MaaApiResponse.NotFound("CopilotOperation", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.CopilotOperationWithIdNotFound!, request.Id))); } var entity = await _dbContext.CopilotOperations.FirstOrDefaultAsync(x => x.Id == id.Value, cancellationToken); if (entity is null) { - return MaaApiResponse.NotFound("CopilotOperation", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.CopilotOperationWithIdNotFound!, request.Id))); } entity.Delete(_currentUserService.GetUserIdentity()!.Value); _dbContext.CopilotOperations.Remove(entity); await _dbContext.SaveChangesAsync(cancellationToken); - return MaaApiResponse.Ok(new EmptyObject(), _currentUserService.GetTrackingId()); + return MaaApiResponse.Ok(null, _currentUserService.GetTrackingId()); } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommandValidator.cs index 51d8ea5..2c5ca56 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Commands/DeleteCopilotOperation/DeleteCopilotOperationCommandValidator.cs @@ -2,16 +2,14 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotOperation.Commands.DeleteCopilotOperation; public class DeleteCopilotOperationCommandValidator : AbstractValidator { - public DeleteCopilotOperationCommandValidator() + public DeleteCopilotOperationCommandValidator(ValidationErrorMessage errorMessage) { RuleFor(x => x.Id) - .NotNull() - .NotEmpty(); + .NotEmpty() + .WithMessage(errorMessage.CopilotOperationIdIsEmpty); } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQuery.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQuery.cs index 202265a..8c08c05 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQuery.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQuery.cs @@ -2,10 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Extensions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotOperation.Queries.GetCopilotOperation; @@ -15,28 +11,35 @@ public record GetCopilotOperationQuery : IRequest> +public class + GetCopilotOperationQueryHandler : IRequestHandler> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICopilotIdService _copilotIdService; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public GetCopilotOperationQueryHandler( IMaaCopilotDbContext dbContext, ICopilotIdService copilotIdService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _copilotIdService = copilotIdService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(GetCopilotOperationQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetCopilotOperationQuery request, + CancellationToken cancellationToken) { var id = _copilotIdService.DecodeId(request.Id!); if (id is null) { - return MaaApiResponse.NotFound("CopilotOperation", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.CopilotOperationWithIdNotFound!, request.Id))); } var entity = await _dbContext.CopilotOperations @@ -44,7 +47,8 @@ public async Task> Handle(GetCopilo .FirstOrDefaultAsync(x => x.Id == id.Value, cancellationToken); if (entity is null) { - return MaaApiResponse.NotFound("CopilotOperation", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.CopilotOperationWithIdNotFound!, request.Id))); } var dto = new GetCopilotOperationQueryDto( diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryDto.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryDto.cs index 090f4a3..4892f02 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryDto.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryDto.cs @@ -8,7 +8,8 @@ namespace MaaCopilotServer.Application.CopilotOperation.Queries.GetCopilotOperat public class GetCopilotOperationQueryDto { - public GetCopilotOperationQueryDto(string id, string stageName, string minimumRequired, string uploadTime, string content, string uploader, string title, string detail) + public GetCopilotOperationQueryDto(string id, string stageName, string minimumRequired, string uploadTime, + string content, string uploader, string title, string detail) { Id = id; StageName = stageName; @@ -20,20 +21,19 @@ public GetCopilotOperationQueryDto(string id, string stageName, string minimumRe Detail = detail; } - [JsonPropertyName("id")] - public string Id { get; } - [JsonPropertyName("stage_name")] - public string StageName { get; } - [JsonPropertyName("minimum_required")] - public string MinimumRequired { get; } - [JsonPropertyName("upload_time")] - public string UploadTime { get; } - [JsonPropertyName("content")] - public string Content { get; } - [JsonPropertyName("title")] - public string Title { get; } - [JsonPropertyName("detail")] - public string Detail { get; } - [JsonPropertyName("uploader")] - public string Uploader { get; } + [JsonPropertyName("id")] public string Id { get; } + + [JsonPropertyName("stage_name")] public string StageName { get; } + + [JsonPropertyName("minimum_required")] public string MinimumRequired { get; } + + [JsonPropertyName("upload_time")] public string UploadTime { get; } + + [JsonPropertyName("content")] public string Content { get; } + + [JsonPropertyName("title")] public string Title { get; } + + [JsonPropertyName("detail")] public string Detail { get; } + + [JsonPropertyName("uploader")] public string Uploader { get; } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryValidator.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryValidator.cs index 8098b3d..bcc4c9a 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/GetCopilotOperation/GetCopilotOperationQueryValidator.cs @@ -2,16 +2,14 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotOperation.Queries.GetCopilotOperation; public class GetCopilotOperationQueryValidator : AbstractValidator { - public GetCopilotOperationQueryValidator() + public GetCopilotOperationQueryValidator(ValidationErrorMessage errorMessage) { RuleFor(x => x.Id) - .NotNull() - .NotEmpty(); + .NotEmpty() + .WithMessage(errorMessage.CopilotOperationIdIsEmpty); } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQuery.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQuery.cs index 67098bd..e261609 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQuery.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQuery.cs @@ -2,10 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Extensions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -24,21 +20,25 @@ public record QueryCopilotOperationsQuery : IRequest>> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICopilotIdService _copilotIdService; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public QueryCopilotOperationsQueryHandler( IMaaCopilotDbContext dbContext, ICopilotIdService copilotIdService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _copilotIdService = copilotIdService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task>> Handle(QueryCopilotOperationsQuery request, CancellationToken cancellationToken) + public async Task>> Handle( + QueryCopilotOperationsQuery request, CancellationToken cancellationToken) { var limit = request.Limit ?? 10; var page = request.Page ?? 1; @@ -48,7 +48,8 @@ public async Task x.StageName.Contains(request.StageName)); } + if (string.IsNullOrEmpty(request.Content) is false) { queryable = queryable.Where(x => x.Content.Contains(request.Content)); } + if (string.IsNullOrEmpty(request.Uploader) is false) { queryable = queryable.Where(x => x.Author.UserName.Contains(request.Uploader)); diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryDto.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryDto.cs index cca500a..21513d7 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryDto.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryDto.cs @@ -8,7 +8,8 @@ namespace MaaCopilotServer.Application.CopilotOperation.Queries.QueryCopilotOper public class QueryCopilotOperationsQueryDto { - public QueryCopilotOperationsQueryDto(string id, string stageName, string minimumRequired, string uploadTime, string uploader, string title, string detail) + public QueryCopilotOperationsQueryDto(string id, string stageName, string minimumRequired, string uploadTime, + string uploader, string title, string detail) { Id = id; StageName = stageName; @@ -19,18 +20,17 @@ public QueryCopilotOperationsQueryDto(string id, string stageName, string minimu Detail = detail; } - [JsonPropertyName("id")] - public string Id { get; } - [JsonPropertyName("stage_name")] - public string StageName { get; } - [JsonPropertyName("minimum_required")] - public string MinimumRequired { get; } - [JsonPropertyName("upload_time")] - public string UploadTime { get; } - [JsonPropertyName("title")] - public string Title { get; } - [JsonPropertyName("detail")] - public string Detail { get; } - [JsonPropertyName("uploader")] - public string Uploader { get; } + [JsonPropertyName("id")] public string Id { get; } + + [JsonPropertyName("stage_name")] public string StageName { get; } + + [JsonPropertyName("minimum_required")] public string MinimumRequired { get; } + + [JsonPropertyName("upload_time")] public string UploadTime { get; } + + [JsonPropertyName("title")] public string Title { get; } + + [JsonPropertyName("detail")] public string Detail { get; } + + [JsonPropertyName("uploader")] public string Uploader { get; } } diff --git a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryValidator.cs b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryValidator.cs index d77a457..b7f98e5 100644 --- a/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotOperation/Queries/QueryCopilotOperations/QueryCopilotOperationsQueryValidator.cs @@ -2,26 +2,26 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Extensions; - namespace MaaCopilotServer.Application.CopilotOperation.Queries.QueryCopilotOperations; public class QueryCopilotOperationsQueryValidator : AbstractValidator { - public QueryCopilotOperationsQueryValidator() + public QueryCopilotOperationsQueryValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.Page).GreaterThanOrEqualTo(1); - RuleFor(x => x.Limit).GreaterThanOrEqualTo(1); - RuleFor(x => x.StageName).NotNull(); - RuleFor(x => x.Content).NotNull(); - RuleFor(x => x.Uploader).NotNull(); + RuleFor(x => x.Page) + .GreaterThanOrEqualTo(1) + .WithMessage(errorMessage.PageIsLessThenOne); + RuleFor(x => x.Limit) + .GreaterThanOrEqualTo(1) + .WithMessage(errorMessage.LimitIsLessThenOne); RuleFor(x => x.UploaderId) - .NotNull().NotEmpty().Must(FluentValidationExtension.BeValidGuid) - .When(x => x.UploaderId != "me" && string.IsNullOrEmpty(x.UploaderId) is false); + .NotEmpty().Must(FluentValidationExtension.BeValidGuid) + .When(x => x.UploaderId != "me" && string.IsNullOrEmpty(x.UploaderId) is false) + .WithMessage(errorMessage.UploaderIdIsInvalid); RuleFor(x => x.UploaderId) - .NotNull().NotEmpty().Equal("me") - .When(x => x.UploaderId == "me"); + .NotEmpty().Equal("me") + .When(x => x.UploaderId == "me") + .WithMessage(errorMessage.UploaderIdIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommand.cs index 17c3ad2..bfad36a 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommand.cs @@ -3,12 +3,8 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; +using Destructurama.Attributed; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.ChangeCopilotUserInfo; @@ -19,27 +15,38 @@ public record ChangeCopilotUserInfoCommand : IRequest> +public class + ChangeCopilotUserInfoCommandHandler : IRequestHandler> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICurrentUserService _currentUserService; + private readonly IMaaCopilotDbContext _dbContext; private readonly ISecretService _secretService; + private readonly ApiErrorMessage _apiErrorMessage; public ChangeCopilotUserInfoCommandHandler( IMaaCopilotDbContext dbContext, ICurrentUserService currentUserService, - ISecretService secretService) + ISecretService secretService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _currentUserService = currentUserService; _secretService = secretService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(ChangeCopilotUserInfoCommand request, CancellationToken cancellationToken) + public async Task> Handle(ChangeCopilotUserInfoCommand request, + CancellationToken cancellationToken) { var userId = Guid.Parse(request.UserId!); var user = await _dbContext.CopilotUsers @@ -47,19 +54,22 @@ public async Task> Handle(ChangeCopilotUserInfoComm if (user is null) { - return MaaApiResponse.NotFound($"User with id {request.UserId}", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.UserWithIdNotFound!, request.UserId))); } var @operator = await _dbContext.CopilotUsers.FirstOrDefaultAsync( x => x.EntityId == _currentUserService.GetUserIdentity(), cancellationToken); if (@operator is null) { - throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId(), + _apiErrorMessage.InternalException)); } if (@operator.UserRole is UserRole.Admin && user.UserRole >= UserRole.Admin) { - return MaaApiResponse.Forbidden(_currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.Forbidden(_currentUserService.GetTrackingId(), + _apiErrorMessage.PermissionDenied)); } if (request.Password is not null) @@ -73,11 +83,12 @@ public async Task> Handle(ChangeCopilotUserInfoComm var exist = _dbContext.CopilotUsers.Any(x => x.Email == request.Email); if (exist) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), $"User with email \"{request.Email}\" already exists"); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.EmailAlreadyInUse)); } } - user.UpdateUserInfo(@operator.EntityId, request.Email, request.UserName, request.Role); + user.UpdateUserInfo(@operator.EntityId, request.Email, request.UserName, Enum.Parse(request.Role!)); _dbContext.CopilotUsers.Update(user); await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommandValidator.cs index 8d48c5c..64790b7 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/ChangeCopilotUserInfo/ChangeCopilotUserInfoCommandValidator.cs @@ -2,31 +2,36 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Extensions; +using MaaCopilotServer.Domain.Enums; namespace MaaCopilotServer.Application.CopilotUser.Commands.ChangeCopilotUserInfo; public class ChangeCopilotUserInfoCommandValidator : AbstractValidator { - public ChangeCopilotUserInfoCommandValidator() + public ChangeCopilotUserInfoCommandValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.UserId).NotNull().NotEmpty().Must(FluentValidationExtension.BeValidGuid); + RuleFor(x => x.UserId) + .NotEmpty().Must(FluentValidationExtension.BeValidGuid) + .WithMessage(errorMessage.UserIdIsInvalid); RuleFor(x => x.Email) - .NotNull().EmailAddress() - .When(x => x.Email is not null); + .NotEmpty().EmailAddress() + .When(x => x.Email is not null) + .WithMessage(errorMessage.EmailIsInvalid); RuleFor(x => x.Password) - .NotNull().NotEmpty().Length(8, 32) - .When(x => x.Password is not null); + .NotEmpty().Length(8, 32) + .When(x => x.Password is not null) + .WithMessage(errorMessage.PasswordIsInvalid); RuleFor(x => x.UserName) - .NotNull().NotEmpty().Length(4, 24) - .When(x => x.UserName is not null); + .NotEmpty().Length(4, 24) + .When(x => x.UserName is not null) + .WithMessage(errorMessage.UsernameIsInvalid); RuleFor(x => x.Role) - .NotNull().NotEmpty().IsInEnum() - .When(x => x.Role is not null); + .NotEmpty().IsEnumName(typeof(UserRole)).NotEqual(UserRole.SuperAdmin.ToString()) + .When(x => x.Role is not null) + .WithMessage(errorMessage.UserRoleIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommand.cs index 19a1846..381f9f6 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommand.cs @@ -3,11 +3,8 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; +using Destructurama.Attributed; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.CreateCopilotUser; @@ -16,37 +13,50 @@ namespace MaaCopilotServer.Application.CopilotUser.Commands.CreateCopilotUser; public record CreateCopilotUserCommand : IRequest> { [JsonPropertyName("email")] public string? Email { get; set; } - [JsonPropertyName("password")] public string? Password { get; set; } + + [JsonPropertyName("password")] + [LogMasked] + public string? Password { get; set; } + [JsonPropertyName("user_name")] public string? UserName { get; set; } - [JsonPropertyName("role"), JsonConverter(typeof(JsonStringEnumConverter))] public UserRole? Role { get; set; } + + [JsonPropertyName("role")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public UserRole? Role { get; set; } } public class CreateCopilotUserCommandHandler : IRequestHandler> { + private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; private readonly IMaaCopilotDbContext _dbContext; private readonly ISecretService _secretService; - private readonly ICurrentUserService _currentUserService; public CreateCopilotUserCommandHandler( IMaaCopilotDbContext dbContext, ISecretService secretService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _secretService = secretService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(CreateCopilotUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateCopilotUserCommand request, + CancellationToken cancellationToken) { var emailColliding = await _dbContext.CopilotUsers.AnyAsync(x => x.Email == request.Email, cancellationToken); if (emailColliding) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), "Email already in use"); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.EmailAlreadyInUse)); } var hashedPassword = _secretService.HashPassword(request.Password!); - var user = new Domain.Entities.CopilotUser(request.Email!, hashedPassword, request.UserName!, request.Role!.Value, _currentUserService.GetUserIdentity()!.Value); + var user = new Domain.Entities.CopilotUser(request.Email!, hashedPassword, request.UserName!, + request.Role!.Value, _currentUserService.GetUserIdentity()!.Value); _dbContext.CopilotUsers.Add(user); await _dbContext.SaveChangesAsync(cancellationToken); return MaaApiResponse.Ok(null, _currentUserService.GetTrackingId()); diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommandValidator.cs index ff07ba0..88ac79c 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/CreateCopilotUser/CreateCopilotUserCommandValidator.cs @@ -2,20 +2,25 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Extensions; using MaaCopilotServer.Domain.Enums; namespace MaaCopilotServer.Application.CopilotUser.Commands.CreateCopilotUser; public class CreateCopilotUserCommandValidator : AbstractValidator { - public CreateCopilotUserCommandValidator() + public CreateCopilotUserCommandValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.Email).NotNull().EmailAddress(); - RuleFor(x => x.Password).NotNull().NotEmpty().Length(8, 32); - RuleFor(x => x.UserName).NotNull().NotEmpty().Length(4, 24); + RuleFor(x => x.Email) + .NotEmpty().EmailAddress() + .WithMessage(errorMessage.EmailIsInvalid); + RuleFor(x => x.Password) + .NotEmpty().Length(8, 32) + .WithMessage(errorMessage.PasswordIsInvalid); + RuleFor(x => x.UserName) + .NotEmpty().Length(4, 24) + .WithMessage(errorMessage.UsernameIsInvalid); RuleFor(x => x.Role) - .NotNull().NotEmpty().IsInEnum().NotEqual(UserRole.SuperAdmin); + .NotEmpty().IsInEnum().NotEqual(UserRole.SuperAdmin) + .WithMessage(errorMessage.UserRoleIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommand.cs index da8a38e..b1752b1 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommand.cs @@ -3,12 +3,7 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.DeleteCopilotUser; @@ -21,36 +16,43 @@ public record DeleteCopilotUserCommand : IRequest> public class DeleteCopilotUserCommandHandler : IRequestHandler> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public DeleteCopilotUserCommandHandler( IMaaCopilotDbContext dbContext, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(DeleteCopilotUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(DeleteCopilotUserCommand request, + CancellationToken cancellationToken) { var userId = Guid.Parse(request.UserId!); var user = await _dbContext.CopilotUsers.FirstOrDefaultAsync(x => x.EntityId == userId, cancellationToken); if (user == null) { - return MaaApiResponse.NotFound($"User \"{userId}\"", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.UserWithIdNotFound!, userId))); } var @operator = await _dbContext.CopilotUsers.FirstOrDefaultAsync( x => x.EntityId == _currentUserService.GetUserIdentity(), cancellationToken); if (@operator is null) { - throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId(), + _apiErrorMessage.InternalException)); } if (@operator.UserRole <= user.UserRole) { - return MaaApiResponse.Forbidden(_currentUserService.GetTrackingId(), "You cannot delete a user with a higher or equal role than you."); + throw new PipelineException(MaaApiResponse.Forbidden(_currentUserService.GetTrackingId(), + _apiErrorMessage.PermissionDenied)); } user.Delete(_currentUserService.GetUserIdentity()!.Value); diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommandValidator.cs index 2fbf0dc..e4dd261 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/DeleteCopilotUser/DeleteCopilotUserCommandValidator.cs @@ -2,15 +2,14 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Extensions; - namespace MaaCopilotServer.Application.CopilotUser.Commands.DeleteCopilotUser; public class DeleteCopilotUserCommandValidator : AbstractValidator { - public DeleteCopilotUserCommandValidator() + public DeleteCopilotUserCommandValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.UserId).NotNull().NotEmpty().Must(FluentValidationExtension.BeValidGuid); + RuleFor(x => x.UserId) + .NotEmpty().Must(FluentValidationExtension.BeValidGuid) + .WithMessage(errorMessage.UserIdIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommand.cs index 0fd34e2..7348926 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommand.cs @@ -3,10 +3,7 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Extensions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; +using Destructurama.Attributed; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.LoginCopilotUser; @@ -14,37 +11,46 @@ namespace MaaCopilotServer.Application.CopilotUser.Commands.LoginCopilotUser; public record LoginCopilotUserCommand : IRequest> { [JsonPropertyName("email")] public string? Email { get; set; } - [JsonPropertyName("password")] public string? Password { get; set; } + + [JsonPropertyName("password")] + [LogMasked] + public string? Password { get; set; } } -public class LoginCopilotUserCommandHandler : IRequestHandler> +public class + LoginCopilotUserCommandHandler : IRequestHandler> { + private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; private readonly IMaaCopilotDbContext _dbContext; private readonly ISecretService _secretService; - private readonly ICurrentUserService _currentUserService; public LoginCopilotUserCommandHandler( IMaaCopilotDbContext dbContext, ISecretService secretService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _secretService = secretService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(LoginCopilotUserCommand request, CancellationToken cancellationToken) + public async Task> Handle(LoginCopilotUserCommand request, + CancellationToken cancellationToken) { var user = await _dbContext.CopilotUsers.FirstOrDefaultAsync(x => x.Email == request.Email, cancellationToken); if (user is null) { - return MaaApiResponse.NotFound("User", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), _apiErrorMessage.LoginFailed)); } var ok = _secretService.VerifyPassword(user.Password, request.Password!); if (ok is false) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), "Invalid password"); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.LoginFailed)); } var (token, expire) = _secretService.GenerateJwtToken(user.EntityId); diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommandValidator.cs index 5a6c302..15bdaab 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserCommandValidator.cs @@ -2,15 +2,17 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotUser.Commands.LoginCopilotUser; public class LoginCopilotUserCommandValidator : AbstractValidator { - public LoginCopilotUserCommandValidator() + public LoginCopilotUserCommandValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.Email).NotNull().EmailAddress(); - RuleFor(x => x.Password).NotNull().NotEmpty().Length(8, 32); + RuleFor(x => x.Email) + .NotEmpty().EmailAddress() + .WithMessage(errorMessage.LoginValidationFail); + RuleFor(x => x.Password) + .NotEmpty().Length(8, 32) + .WithMessage(errorMessage.LoginValidationFail); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserDto.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserDto.cs index 75c95da..ef86aae 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserDto.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/LoginCopilotUser/LoginCopilotUserDto.cs @@ -15,10 +15,9 @@ public LoginCopilotUserDto(string token, string validBefore, string userName) UserName = userName; } - [JsonPropertyName("user_name")] - public string UserName { get; set; } - [JsonPropertyName("token")] - public string Token { get; set; } - [JsonPropertyName("valid_before")] - public string ValidBefore { get; set; } + [JsonPropertyName("user_name")] public string UserName { get; set; } + + [JsonPropertyName("token")] public string Token { get; set; } + + [JsonPropertyName("valid_before")] public string ValidBefore { get; set; } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommand.cs index 5984c1d..50156af 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommand.cs @@ -3,12 +3,7 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserInfo; @@ -20,27 +15,33 @@ public record UpdateCopilotUserInfoCommand : IRequest> +public class + UpdateCopilotUserInfoCommandHandler : IRequestHandler> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public UpdateCopilotUserInfoCommandHandler( IMaaCopilotDbContext dbContext, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(UpdateCopilotUserInfoCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateCopilotUserInfoCommand request, + CancellationToken cancellationToken) { var user = await _dbContext.CopilotUsers .FirstOrDefaultAsync(x => x.EntityId == _currentUserService.GetUserIdentity(), cancellationToken); if (user is null) { - throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId(), + _apiErrorMessage.InternalException)); } if (string.IsNullOrEmpty(request.Email)) @@ -48,7 +49,8 @@ public async Task> Handle(UpdateCopilotUserInfoComm var exist = _dbContext.CopilotUsers.Any(x => x.Email == request.Email); if (exist) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), $"User with email \"{request.Email}\" already exists"); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.EmailAlreadyInUse)); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommandValidator.cs index 6b95ca8..35cf4eb 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserInfo/UpdateCopilotUserInfoCommandValidator.cs @@ -2,20 +2,20 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserInfo; public class UpdateCopilotUserInfoCommandValidator : AbstractValidator { - public UpdateCopilotUserInfoCommandValidator() + public UpdateCopilotUserInfoCommandValidator(ValidationErrorMessage errorMessage) { RuleFor(x => x.Email) - .NotNull().EmailAddress() - .When(x => x.Email is not null); + .EmailAddress() + .When(x => x.Email is not null) + .WithMessage(errorMessage.EmailIsInvalid); RuleFor(x => x.UserName) - .NotNull().NotEmpty().Length(4, 24) - .When(x => x.UserName is not null); + .NotEmpty().Length(4, 24) + .When(x => x.UserName is not null) + .WithMessage(errorMessage.UsernameIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommand.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommand.cs index dd146be..bbfd605 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommand.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommand.cs @@ -3,12 +3,8 @@ // Licensed under the AGPL-3.0 license. using System.Text.Json.Serialization; -using MaaCopilotServer.Application.Common.Exceptions; -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MaaCopilotServer.Application.Common.Security; +using Destructurama.Attributed; using MaaCopilotServer.Domain.Enums; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserPassword; @@ -16,43 +12,53 @@ namespace MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserPas [Authorized(UserRole.User)] public record UpdateCopilotUserPasswordCommand : IRequest> { - [JsonPropertyName("original_password")] public string? OriginalPassword { get; set; } + [JsonPropertyName("original_password")] + [NotLogged] + public string? OriginalPassword { get; set; } - [JsonPropertyName("new_password")] public string? NewPassword { get; set; } + [JsonPropertyName("new_password")] + [NotLogged] + public string? NewPassword { get; set; } } public class UpdateCopilotUserPasswordCommandHandler : IRequestHandler> { + private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; private readonly IMaaCopilotDbContext _dbContext; private readonly ISecretService _secretService; - private readonly ICurrentUserService _currentUserService; public UpdateCopilotUserPasswordCommandHandler( IMaaCopilotDbContext dbContext, ISecretService secretService, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _secretService = secretService; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(UpdateCopilotUserPasswordCommand request, CancellationToken cancellationToken) + public async Task> Handle(UpdateCopilotUserPasswordCommand request, + CancellationToken cancellationToken) { var user = await _dbContext.CopilotUsers .FirstOrDefaultAsync(x => x.EntityId == _currentUserService.GetUserIdentity()!.Value, cancellationToken); if (user is null) { - throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId())); + throw new PipelineException(MaaApiResponse.InternalError(_currentUserService.GetTrackingId(), + _apiErrorMessage.InternalException)); } var ok = _secretService.VerifyPassword(user!.Password, request.OriginalPassword!); if (ok is false) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), "Invalid password"); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.PasswordInvalid)); } var hash = _secretService.HashPassword(request.NewPassword!); diff --git a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommandValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommandValidator.cs index fb1b99f..66357e9 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommandValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Commands/UpdateCopilotUserPassword/UpdateCopilotUserPasswordCommandValidator.cs @@ -2,15 +2,17 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotUser.Commands.UpdateCopilotUserPassword; public class UpdateCopilotUserPasswordCommandValidator : AbstractValidator { - public UpdateCopilotUserPasswordCommandValidator() + public UpdateCopilotUserPasswordCommandValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.OriginalPassword).NotNull().NotEmpty().Length(8, 32); - RuleFor(x => x.NewPassword).NotNull().NotEmpty().Length(8, 32); + RuleFor(x => x.OriginalPassword) + .NotEmpty().Length(8, 32) + .WithMessage(errorMessage.PasswordIsInvalid); + RuleFor(x => x.NewPassword) + .NotEmpty().Length(8, 32) + .WithMessage(errorMessage.PasswordIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserDto.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserDto.cs index ad5e113..0912d04 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserDto.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserDto.cs @@ -17,13 +17,13 @@ public GetCopilotUserDto(Guid id, string userName, UserRole userRole, int upload UploadCount = uploadCount; } - [JsonPropertyName("id")] - public Guid Id { get; set; } - [JsonPropertyName("user_name")] - public string UserName { get; set; } + [JsonPropertyName("id")] public Guid Id { get; set; } + + [JsonPropertyName("user_name")] public string UserName { get; set; } + [JsonPropertyName("role")] [JsonConverter(typeof(JsonStringEnumConverter))] public UserRole UserRole { get; set; } - [JsonPropertyName("upload_count")] - public int UploadCount { get; set; } + + [JsonPropertyName("upload_count")] public int UploadCount { get; set; } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQuery.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQuery.cs index 1ebb341..d844d2e 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQuery.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQuery.cs @@ -2,9 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; using Microsoft.EntityFrameworkCore; namespace MaaCopilotServer.Application.CopilotUser.Queries.GetCopilotUser; @@ -16,18 +13,22 @@ public record GetCopilotUserQuery : IRequest> public class GetCopilotUserQueryHandler : IRequestHandler> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICurrentUserService _currentUserService; + private readonly ApiErrorMessage _apiErrorMessage; + private readonly IMaaCopilotDbContext _dbContext; public GetCopilotUserQueryHandler( IMaaCopilotDbContext dbContext, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + ApiErrorMessage apiErrorMessage) { _dbContext = dbContext; _currentUserService = currentUserService; + _apiErrorMessage = apiErrorMessage; } - public async Task> Handle(GetCopilotUserQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetCopilotUserQuery request, + CancellationToken cancellationToken) { Guid userId; if (request.UserId == "me") @@ -35,19 +36,23 @@ public async Task> Handle(GetCopilotUserQuery var id = _currentUserService.GetUserIdentity(); if (id is null) { - return MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), "User is not authenticated."); + throw new PipelineException(MaaApiResponse.BadRequest(_currentUserService.GetTrackingId(), + _apiErrorMessage.MeNotFound)); } + userId = id.Value; } else { userId = Guid.Parse(request.UserId!); } + var user = await _dbContext.CopilotUsers.FirstOrDefaultAsync(x => x.EntityId == userId, cancellationToken); if (user is null) { - return MaaApiResponse.NotFound($"User with id {request.UserId}", _currentUserService.GetTrackingId()); + throw new PipelineException(MaaApiResponse.NotFound(_currentUserService.GetTrackingId(), + string.Format(_apiErrorMessage.UserWithIdNotFound!, request.UserId))); } var uploadCount = await _dbContext.CopilotOperations diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQueryValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQueryValidator.cs index 31cba05..81d9151 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQueryValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/GetCopilotUser/GetCopilotUserQueryValidator.cs @@ -2,20 +2,19 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; -using MaaCopilotServer.Application.Common.Extensions; - namespace MaaCopilotServer.Application.CopilotUser.Queries.GetCopilotUser; public class GetCopilotUserQueryValidator : AbstractValidator { - public GetCopilotUserQueryValidator() + public GetCopilotUserQueryValidator(ValidationErrorMessage errorMessage) { RuleFor(x => x.UserId) .NotNull().NotEmpty().Must(FluentValidationExtension.BeValidGuid) - .When(x => x.UserId != "me"); + .When(x => x.UserId != "me") + .WithMessage(errorMessage.UserIdIsInvalid); RuleFor(x => x.UserId) .NotNull().NotEmpty().Equal("me") - .When(x => x.UserId == "me"); + .When(x => x.UserId == "me") + .WithMessage(errorMessage.UserIdIsInvalid); } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserDto.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserDto.cs index 29c8b6f..d924f04 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserDto.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserDto.cs @@ -16,10 +16,9 @@ public QueryCopilotUserDto(Guid id, string userName, UserRole userRole) UserRole = userRole; } - [JsonPropertyName("id")] - public Guid Id { get; set; } - [JsonPropertyName("user_name")] - public string UserName { get; set; } - [JsonPropertyName("role")] - public UserRole UserRole { get; set; } + [JsonPropertyName("id")] public Guid Id { get; set; } + + [JsonPropertyName("user_name")] public string UserName { get; set; } + + [JsonPropertyName("role")] public UserRole UserRole { get; set; } } diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQuery.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQuery.cs index aeff0e8..42d1792 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQuery.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQuery.cs @@ -2,9 +2,6 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using MaaCopilotServer.Application.Common.Interfaces; -using MaaCopilotServer.Application.Common.Models; -using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -17,10 +14,11 @@ public record QueryCopilotUserQuery : IRequest>> +public class QueryCopilotUserQueryHandler : IRequestHandler>> { - private readonly IMaaCopilotDbContext _dbContext; private readonly ICurrentUserService _currentUserService; + private readonly IMaaCopilotDbContext _dbContext; public QueryCopilotUserQueryHandler( IMaaCopilotDbContext dbContext, @@ -30,7 +28,8 @@ public QueryCopilotUserQueryHandler( _currentUserService = currentUserService; } - public async Task>> Handle(QueryCopilotUserQuery request, CancellationToken cancellationToken) + public async Task>> Handle(QueryCopilotUserQuery request, + CancellationToken cancellationToken) { var limit = request.Limit ?? 10; var page = request.Page ?? 1; diff --git a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQueryValidator.cs b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQueryValidator.cs index cafdceb..fcff591 100644 --- a/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQueryValidator.cs +++ b/src/MaaCopilotServer.Application/CopilotUser/Queries/QueryCopilotUser/QueryCopilotUserQueryValidator.cs @@ -2,16 +2,17 @@ // MaaCopilotServer belongs to the MAA organization. // Licensed under the AGPL-3.0 license. -using FluentValidation; - namespace MaaCopilotServer.Application.CopilotUser.Queries.QueryCopilotUser; public class QueryCopilotUserQueryValidator : AbstractValidator { - public QueryCopilotUserQueryValidator() + public QueryCopilotUserQueryValidator(ValidationErrorMessage errorMessage) { - RuleFor(x => x.Page).GreaterThanOrEqualTo(1); - RuleFor(x => x.Limit).GreaterThanOrEqualTo(1); - RuleFor(x => x.UserName).NotNull(); + RuleFor(x => x.Page) + .GreaterThanOrEqualTo(1) + .WithMessage(errorMessage.PageIsLessThenOne); + RuleFor(x => x.Limit) + .GreaterThanOrEqualTo(1) + .WithMessage(errorMessage.LimitIsLessThenOne); } } diff --git a/src/MaaCopilotServer.Application/Global.cs b/src/MaaCopilotServer.Application/Global.cs new file mode 100644 index 0000000..5b082c2 --- /dev/null +++ b/src/MaaCopilotServer.Application/Global.cs @@ -0,0 +1,15 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +global using MaaCopilotServer.Application.Common.Exceptions; +global using MaaCopilotServer.Application.Common.Behaviours; +global using MaaCopilotServer.Application.Common.Interfaces; +global using MaaCopilotServer.Application.Common.Security; +global using MaaCopilotServer.Application.Common.Extensions; +global using MaaCopilotServer.Application.Common.Enum; +global using MaaCopilotServer.Application.Common.Models; +global using MaaCopilotServer.Resources; +global using MediatR; +global using MediatR.Pipeline; +global using FluentValidation; diff --git a/src/MaaCopilotServer.Application/MaaCopilotServer.Application.csproj b/src/MaaCopilotServer.Application/MaaCopilotServer.Application.csproj index ed36a5f..ad97c83 100644 --- a/src/MaaCopilotServer.Application/MaaCopilotServer.Application.csproj +++ b/src/MaaCopilotServer.Application/MaaCopilotServer.Application.csproj @@ -1,29 +1,31 @@ - - net6.0 - MaaCopilotServer.Application - MaaCopilotServer.Application - enable - enable - + + net6.0 + MaaCopilotServer.Application + MaaCopilotServer.Application + enable + enable + - - true - - - - - + + true + - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/MaaCopilotServer.Infrastructure/Database/MaaCopilotDbContext.cs b/src/MaaCopilotServer.Infrastructure/Database/MaaCopilotDbContext.cs index 5a6f75c..9599937 100644 --- a/src/MaaCopilotServer.Infrastructure/Database/MaaCopilotDbContext.cs +++ b/src/MaaCopilotServer.Infrastructure/Database/MaaCopilotDbContext.cs @@ -27,7 +27,7 @@ public MaaCopilotDbContext(IConfiguration configuration) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - var conn = _connectionString.NotNull(); + var conn = _connectionString.IsNotNull(); optionsBuilder.UseNpgsql(conn); base.OnConfiguring(optionsBuilder); } diff --git a/src/MaaCopilotServer.Resources/ApiErrorMessage.en.resx b/src/MaaCopilotServer.Resources/ApiErrorMessage.en.resx new file mode 100644 index 0000000..3ea6fa3 --- /dev/null +++ b/src/MaaCopilotServer.Resources/ApiErrorMessage.en.resx @@ -0,0 +1,41 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Can not find operation with id {0} + + + Email has been used + + + Internal error + + + Invalid Email or Password + + + Can not find "me" before login + + + Invalid Password + + + Permission denied + + + Unauthorized, please login first + + + Can not find user with id {0} + + diff --git a/src/MaaCopilotServer.Resources/ApiErrorMessage.resx b/src/MaaCopilotServer.Resources/ApiErrorMessage.resx new file mode 100644 index 0000000..86961fc --- /dev/null +++ b/src/MaaCopilotServer.Resources/ApiErrorMessage.resx @@ -0,0 +1,48 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 权限不足 + + + 未鉴权,请先登录 + + + 未登录,找不到 me + + + 邮箱已被使用 + + + 邮箱或密码错误 + + + 密码错误 + + + 找不到 ID 为 {0} 的用户 + + + 找不到 ID 为 {0} 的作业 + + + 出现内部错误 + + diff --git a/src/MaaCopilotServer.Resources/ConfigureResources.cs b/src/MaaCopilotServer.Resources/ConfigureResources.cs new file mode 100644 index 0000000..5a44e0b --- /dev/null +++ b/src/MaaCopilotServer.Resources/ConfigureResources.cs @@ -0,0 +1,18 @@ +// This file is a part of MaaCopilotServer project. +// MaaCopilotServer belongs to the MAA organization. +// Licensed under the AGPL-3.0 license. + +using Microsoft.Extensions.DependencyInjection; + +namespace MaaCopilotServer.Resources; + +public static class ConfigureResources +{ + public static IServiceCollection AddResources(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/src/MaaCopilotServer.Resources/MaaCopilotServer.Resources.csproj b/src/MaaCopilotServer.Resources/MaaCopilotServer.Resources.csproj new file mode 100644 index 0000000..f293163 --- /dev/null +++ b/src/MaaCopilotServer.Resources/MaaCopilotServer.Resources.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + true + false + false + + + diff --git a/src/MaaCopilotServer.Resources/ValidationErrorMessage.en.resx b/src/MaaCopilotServer.Resources/ValidationErrorMessage.en.resx new file mode 100644 index 0000000..adb3818 --- /dev/null +++ b/src/MaaCopilotServer.Resources/ValidationErrorMessage.en.resx @@ -0,0 +1,50 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Operation id can not be empty + + + Operation id can not be null + + + Operation JSON is invalid + + + Invalid email address + + + Limit can not be less then 1 + + + Email or Password can not be empty + + + Page can not be less then 1 + + + Invalid password, should have 8-32 characters + + + UploaderId must be a valid GUID or me + + + Invalid user id + + + Invalid user name, should have 4-24 characters + + + Invalid user role name + + diff --git a/src/MaaCopilotServer.Resources/ValidationErrorMessage.resx b/src/MaaCopilotServer.Resources/ValidationErrorMessage.resx new file mode 100644 index 0000000..ecd9492 --- /dev/null +++ b/src/MaaCopilotServer.Resources/ValidationErrorMessage.resx @@ -0,0 +1,57 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 页码不可小于 1 + + + 每页数量不可小于 1 + + + UploaderId 必须是有效的 Guid 或者 me + + + 作业 ID 不可为 Null + + + 作业 JSON 不合法 + + + 作业 ID 不可为空 + + + 邮箱格式不合法 + + + 密码格式不合法,至少需要 8 位,最多 32 位 + + + 用户名不合法,至少需要 4 位,最多 24 位 + + + 权限组名称不合法 + + + 用户 ID 不合法 + + + 邮箱或密码不可为空 + +