diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml index 2ec673547..f2c536b46 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml @@ -168,22 +168,46 @@ modules: downstream: reports-service/reports auth: true - - upstream: / - method: GET + - upstream: /{reportId} + method: DELETE use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/{reportId} auth: true - - upstream: / - method: PUT + - upstream: /search + method: POST use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/search auth: true - - upstream: /{reportId} + - upstream: /{reportId}/cancel + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/cancel + auth: true + + - upstream: /{reportId}/start-review + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/start-review + auth: true + + - upstream: /{reportId}/resolve + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/resolve + auth: true + + - upstream: /{reportId}/reject + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/reject + auth: true + + - upstream: /students/{studentId} method: GET use: downstream - downstream: reports-service/reports/{reportId} + downstream: reports-service/reports/students/{studentId} auth: true services: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml index 2323905eb..d84e63a1d 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml @@ -168,22 +168,46 @@ modules: downstream: reports-service/reports auth: true - - upstream: / - method: GET + - upstream: /{reportId} + method: DELETE use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/{reportId} auth: true - - upstream: / - method: PUT + - upstream: /search + method: POST use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/search auth: true - - upstream: /{reportId} + - upstream: /{reportId}/cancel + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/cancel + auth: true + + - upstream: /{reportId}/start-review + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/start-review + auth: true + + - upstream: /{reportId}/resolve + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/resolve + auth: true + + - upstream: /{reportId}/reject + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/reject + auth: true + + - upstream: /students/{studentId} method: GET use: downstream - downstream: reports-service/reports/{reportId} + downstream: reports-service/reports/students/{studentId} auth: true services: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml index 8dfc7384e..ab080fde2 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml @@ -145,22 +145,46 @@ modules: downstream: reports-service/reports auth: true - - upstream: / - method: GET + - upstream: /{reportId} + method: DELETE use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/{reportId} auth: true - - upstream: / - method: PUT + - upstream: /search + method: POST use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/search auth: true - - upstream: /{reportId} + - upstream: /{reportId}/cancel + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/cancel + auth: true + + - upstream: /{reportId}/start-review + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/start-review + auth: true + + - upstream: /{reportId}/resolve + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/resolve + auth: true + + - upstream: /{reportId}/reject + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/reject + auth: true + + - upstream: /students/{studentId} method: GET use: downstream - downstream: reports-service/reports/{reportId} + downstream: reports-service/reports/students/{studentId} auth: true services: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 0170714a5..a3ea70302 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -147,22 +147,46 @@ modules: downstream: reports-service/reports auth: true - - upstream: / - method: GET + - upstream: /{reportId} + method: DELETE use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/{reportId} auth: true - - upstream: / - method: PUT + - upstream: /search + method: POST use: downstream - downstream: reports-service/reports + downstream: reports-service/reports/search auth: true - - upstream: /{reportId} + - upstream: /{reportId}/cancel + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/cancel + auth: true + + - upstream: /{reportId}/start-review + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/start-review + auth: true + + - upstream: /{reportId}/resolve + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/resolve + auth: true + + - upstream: /{reportId}/reject + method: POST + use: downstream + downstream: reports-service/reports/{reportId}/reject + auth: true + + - upstream: /students/{studentId} method: GET use: downstream - downstream: reports-service/reports/{reportId} + downstream: reports-service/reports/students/{studentId} auth: true services: diff --git a/MiniSpace.Services.Reports/MiniSpace.Services.Reports.sln b/MiniSpace.Services.Reports/MiniSpace.Services.Reports.sln new file mode 100644 index 000000000..b6dc505a9 --- /dev/null +++ b/MiniSpace.Services.Reports/MiniSpace.Services.Reports.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{62C95F84-EA55-4778-B3B2-A86F4CAC3251}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reports.Api", "src\MiniSpace.Services.Reports.Api\MiniSpace.Services.Reports.Api.csproj", "{1B494198-CB0A-49A0-95A8-4AAE613CEACA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reports.Application", "src\MiniSpace.Services.Reports.Application\MiniSpace.Services.Reports.Application.csproj", "{E9BDACF7-D03A-4CAF-B127-80444BB751CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reports.Core", "src\MiniSpace.Services.Reports.Core\MiniSpace.Services.Reports.Core.csproj", "{C1F71C74-5021-4FC8-9EB9-7222D093ABF9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reports.Infrastructure", "src\MiniSpace.Services.Reports.Infrastructure\MiniSpace.Services.Reports.Infrastructure.csproj", "{F4851582-777D-473D-8A50-AB4FA57EC91A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1B494198-CB0A-49A0-95A8-4AAE613CEACA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B494198-CB0A-49A0-95A8-4AAE613CEACA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B494198-CB0A-49A0-95A8-4AAE613CEACA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B494198-CB0A-49A0-95A8-4AAE613CEACA}.Release|Any CPU.Build.0 = Release|Any CPU + {E9BDACF7-D03A-4CAF-B127-80444BB751CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9BDACF7-D03A-4CAF-B127-80444BB751CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9BDACF7-D03A-4CAF-B127-80444BB751CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9BDACF7-D03A-4CAF-B127-80444BB751CD}.Release|Any CPU.Build.0 = Release|Any CPU + {C1F71C74-5021-4FC8-9EB9-7222D093ABF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1F71C74-5021-4FC8-9EB9-7222D093ABF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1F71C74-5021-4FC8-9EB9-7222D093ABF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1F71C74-5021-4FC8-9EB9-7222D093ABF9}.Release|Any CPU.Build.0 = Release|Any CPU + {F4851582-777D-473D-8A50-AB4FA57EC91A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4851582-777D-473D-8A50-AB4FA57EC91A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4851582-777D-473D-8A50-AB4FA57EC91A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4851582-777D-473D-8A50-AB4FA57EC91A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1B494198-CB0A-49A0-95A8-4AAE613CEACA} = {62C95F84-EA55-4778-B3B2-A86F4CAC3251} + {E9BDACF7-D03A-4CAF-B127-80444BB751CD} = {62C95F84-EA55-4778-B3B2-A86F4CAC3251} + {C1F71C74-5021-4FC8-9EB9-7222D093ABF9} = {62C95F84-EA55-4778-B3B2-A86F4CAC3251} + {F4851582-777D-473D-8A50-AB4FA57EC91A} = {62C95F84-EA55-4778-B3B2-A86F4CAC3251} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Reports/scripts/build.sh b/MiniSpace.Services.Reports/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Reports/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Reports/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Reports/scripts/dockerize-tag-push.sh new file mode 100644 index 000000000..45d628c19 --- /dev/null +++ b/MiniSpace.Services.Reports/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.reports:latest . + +docker tag minispace.services.reports:latest adrianvsaint/minispace.services.reports:latest + +docker push adrianvsaint/minispace.services.reports:latest diff --git a/MiniSpace.Services.Reports/scripts/start.sh b/MiniSpace.Services.Reports/scripts/start.sh new file mode 100644 index 000000000..59c777d46 --- /dev/null +++ b/MiniSpace.Services.Reports/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Reports.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Reports/scripts/test.sh b/MiniSpace.Services.Reports/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Reports/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/MiniSpace.Services.Reports.Api.csproj b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/MiniSpace.Services.Reports.Api.csproj new file mode 100644 index 000000000..9ae62c447 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/MiniSpace.Services.Reports.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + disable + enable + true + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Program.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Program.cs new file mode 100644 index 000000000..a1ee78d14 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Program.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using MiniSpace.Services.Reports.Application; +using MiniSpace.Services.Reports.Application.Commands; +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Application.Queries; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Wrappers; +using MiniSpace.Services.Reports.Infrastructure; + +namespace MiniSpace.Services.Reports.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => services + .AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure() + .Build()) + .Configure(app => app + .UseInfrastructure() + .UseEndpoints(endpoints => endpoints + .Post("reports/search", async (cmd, ctx) => + { + var pagedResult = await ctx.RequestServices.GetService().BrowseReportsAsync(cmd); + await ctx.Response.WriteJsonAsync(pagedResult); + }) + ) + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Post("reports", afterDispatch: (cmd, ctx) + => ctx.Response.Created($"reports/{cmd.ReportId}")) + .Delete("reports/{reportId}") + .Post("reports/{reportId}/cancel") + .Post("reports/{reportId}/start-review") + .Post("reports/{reportId}/resolve") + .Post("reports/{reportId}/reject") + .Get>>("reports/students/{studentId}") + )) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Properties/launchSettings.json b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Properties/launchSettings.json new file mode 100644 index 000000000..b1b8edd5b --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5005" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Reports": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.Development.json b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.Development.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.docker.json b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.docker.json new file mode 100644 index 000000000..2d194e418 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace reports Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "reports-service", + "address": "reports-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "reports-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": {} + }, + "jwt": { + "certificate": { + "location": "", + "password": "", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": true, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://elk:9200" + }, + "file": { + "enabled": false, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://seq:5341", + "apiKey": "secret" + } + }, + "jaeger": { + "enabled": true, + "serviceName": "reports", + "udpHost": "jaeger", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://influx:8086", + "database": "minispace", + "env": "docker", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "reports-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "reports-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "rabbitmq" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "reports" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "reports-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "reports:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": false, + "url": "http://vault:8200", + "kv": { + "enabled": false + }, + "pki": { + "enabled": false + }, + "lease": { + "mongo": { + "enabled": false + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.json b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.local.json b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.local.json new file mode 100644 index 000000000..6215f26c4 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Reports Service", + "service": "reports-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "reports-service", + "address": "docker.for.win.localhost", + "port": "5005", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "reports-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": {}, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "certificate": { + "location": "certs/localhost.pfx", + "password": "test", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": false, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "level": "information", + "excludePaths": ["/", "/ping", "/metrics"], + "excludeProperties": [ + "api_key", + "access_key", + "ApiKey", + "ApiSecret", + "ClientId", + "ClientSecret", + "ConnectionString", + "Password", + "Email", + "Login", + "Secret", + "Token" + ], + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://localhost:9200" + }, + "file": { + "enabled": true, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://localhost:5341", + "apiKey": "secret" + }, + "tags": {} + }, + "jaeger": { + "enabled": true, + "serviceName": "reports", + "udpHost": "localhost", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://localhost:8086", + "database": "minispace", + "env": "local", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "reports-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "reports-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "localhost" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "reports" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "reports-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "reports:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": true, + "url": "http://localhost:8200", + "authType": "token", + "token": "secret", + "username": "user", + "password": "secret", + "kv": { + "enabled": true, + "engineVersion": 2, + "mountPoint": "kv", + "path": "reports-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "reports-service", + "commonName": "reports-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "reports-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CancelReport.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CancelReport.cs new file mode 100644 index 000000000..a054fb34c --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CancelReport.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class CancelReport: ICommand + { + public Guid ReportId { get; } + + public CancelReport(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CreateReport.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CreateReport.cs new file mode 100644 index 000000000..5e7759caf --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/CreateReport.cs @@ -0,0 +1,28 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class CreateReport: ICommand + { + public Guid ReportId { get; } + public Guid IssuerId { get; set; } + public Guid TargetId { get; set; } + public Guid TargetOwnerId { get; set; } + public string ContextType { get; set; } + public string Category { get; set; } + public string Reason { get; private set; } + + public CreateReport(Guid reportId, Guid issuerId, Guid targetId, Guid targetOwnerId, string contextType, + string category, string reason) + { + ReportId = reportId == Guid.Empty ? Guid.NewGuid() : reportId; + IssuerId = issuerId; + TargetId = targetId; + TargetOwnerId = targetOwnerId; + ContextType = contextType; + Category = category; + Reason = reason; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/DeleteReport.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/DeleteReport.cs new file mode 100644 index 000000000..9d028fb6a --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/DeleteReport.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class DeleteReport: ICommand + { + public Guid ReportId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CancelReportHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CancelReportHandler.cs new file mode 100644 index 000000000..3a19f2f30 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CancelReportHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class CancelReportHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public CancelReportHandler(IReportRepository reportRepository, IAppContext appContext, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CancelReport command, CancellationToken cancellationToken) + { + var report = await _reportRepository.GetAsync(command.ReportId); + if (report is null) + { + throw new ReportNotFoundException(command.ReportId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != report.IssuerId) + { + throw new UnauthorizedReportAccessAttemptException(report.Id, identity.Id); + } + + report.Cancel(_dateTimeProvider.Now); + await _reportRepository.UpdateAsync(report); + await _messageBroker.PublishAsync(new ReportCancelled(report.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CreateReportHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CreateReportHandler.cs new file mode 100644 index 000000000..2f8cbeda7 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/CreateReportHandler.cs @@ -0,0 +1,48 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class CreateReportHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IReportValidator _reportValidator; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public CreateReportHandler(IReportRepository reportRepository, IReportValidator reportValidator, + IAppContext appContext, IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _reportValidator = reportValidator; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CreateReport command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != command.IssuerId) + { + throw new UnauthorizedReportCreationAttemptException(identity.Id, command.IssuerId); + } + + var contextType = _reportValidator.ParseContextType(command.ContextType); + var category = _reportValidator.ParseCategory(command.Category); + _reportValidator.ValidateReason(command.Reason); + var activeStudentReports = await _reportRepository.GetStudentActiveReportsAsync(command.IssuerId); + _reportValidator.ValidateActiveReports(activeStudentReports.Count()); + + var report = Report.Create(command.ReportId, command.IssuerId, command.TargetId, command.TargetOwnerId, + contextType, category, command.Reason, _dateTimeProvider.Now); + await _reportRepository.AddAsync(report); + await _messageBroker.PublishAsync(new ReportCreated(report.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/DeleteReportHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/DeleteReportHandler.cs new file mode 100644 index 000000000..705d13440 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/DeleteReportHandler.cs @@ -0,0 +1,41 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class DeleteReportHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteReportHandler(IReportRepository reportRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteReport command, CancellationToken cancellationToken) + { + var report = await _reportRepository.GetAsync(command.ReportId); + if (report is null) + { + throw new ReportNotFoundException(command.ReportId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && !identity.IsAdmin) + { + throw new UnauthorizedReportAccessAttemptException(report.Id, identity.Id); + } + + await _reportRepository.DeleteAsync(report.Id); + await _messageBroker.PublishAsync(new ReportDeleted(report.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/RejectReportHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/RejectReportHandler.cs new file mode 100644 index 000000000..c54f4e73b --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/RejectReportHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class RejectReportHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public RejectReportHandler(IReportRepository reportRepository, IAppContext appContext, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(RejectReport command, CancellationToken cancellationToken) + { + var report = await _reportRepository.GetAsync(command.ReportId); + if (report is null) + { + throw new ReportNotFoundException(command.ReportId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != report.ReviewerId) + { + throw new UnauthorizedReportAccessAttemptException(report.Id, identity.Id); + } + + report.Reject(_dateTimeProvider.Now); + await _reportRepository.UpdateAsync(report); + await _messageBroker.PublishAsync(new ReportResolved(report.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/ResolveReportHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/ResolveReportHandler.cs new file mode 100644 index 000000000..fc6d804b2 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/ResolveReportHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class ResolveReportHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public ResolveReportHandler(IReportRepository reportRepository, IAppContext appContext, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(ResolveReport command, CancellationToken cancellationToken) + { + var report = await _reportRepository.GetAsync(command.ReportId); + if (report is null) + { + throw new ReportNotFoundException(command.ReportId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != report.ReviewerId) + { + throw new UnauthorizedReportAccessAttemptException(report.Id, identity.Id); + } + + report.Resolve(_dateTimeProvider.Now); + await _reportRepository.UpdateAsync(report); + await _messageBroker.PublishAsync(new ReportResolved(report.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/StartReportReviewHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/StartReportReviewHandler.cs new file mode 100644 index 000000000..5d2ec7690 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/Handlers/StartReportReviewHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Commands.Handlers +{ + public class StartReportReviewHandler : ICommandHandler + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public StartReportReviewHandler(IReportRepository reportRepository, IAppContext appContext, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _reportRepository = reportRepository; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StartReportReview command, CancellationToken cancellationToken) + { + var report = await _reportRepository.GetAsync(command.ReportId); + if (report is null) + { + throw new ReportNotFoundException(command.ReportId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && !identity.IsAdmin) + { + throw new UnauthorizedReportAccessAttemptException(report.Id, identity.Id); + } + + report.StartReview(command.ReviewerId, _dateTimeProvider.Now); + await _reportRepository.UpdateAsync(report); + await _messageBroker.PublishAsync(new ReportReviewStarted(report.Id, command.ReviewerId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/RejectReport.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/RejectReport.cs new file mode 100644 index 000000000..e5b47ca22 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/RejectReport.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class RejectReport : ICommand + { + public Guid ReportId { get; } + + public RejectReport(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/ResolveReport.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/ResolveReport.cs new file mode 100644 index 000000000..304ac20f6 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/ResolveReport.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class ResolveReport : ICommand + { + public Guid ReportId { get; } + + public ResolveReport(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/SearchReports.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/SearchReports.cs new file mode 100644 index 000000000..388e493e9 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/SearchReports.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reports.Application.DTO; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class SearchReports : ICommand + { + public IEnumerable ContextTypes { get; set; } + public IEnumerable States { get; set; } + public Guid ReviewerId { get; set; } + public PageableDto Pageable { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/StartReportReview.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/StartReportReview.cs new file mode 100644 index 000000000..e585861a4 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Commands/StartReportReview.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reports.Application.Commands +{ + public class StartReportReview: ICommand + { + public Guid ReportId { get; } + public Guid ReviewerId { get; set; } + + public StartReportReview(Guid reportId, Guid reviewerId) + { + ReportId = reportId; + ReviewerId = reviewerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/ContractAttribute.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/ContractAttribute.cs new file mode 100644 index 000000000..1f8f0e698 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reports.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/PageableDto.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/PageableDto.cs new file mode 100644 index 000000000..76c906d29 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/PageableDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Reports.Application.DTO +{ + public class PageableDto + { + public int Page { get; set; } + public int Size { get; set; } + public SortDto Sort { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/ReportDto.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/ReportDto.cs new file mode 100644 index 000000000..4e0a1198f --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/ReportDto.cs @@ -0,0 +1,39 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Application.DTO +{ + public class ReportDto + { + public Guid Id { get; set; } + public Guid IssuerId { get; set; } + public Guid TargetId { get; set; } + public Guid TargetOwnerId { get; set; } + public string ContextType { get; set; } + public string Category { get; set; } + public string Reason { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public Guid? ReviewerId { get; set; } + + public ReportDto() + { + + } + + public ReportDto(Report report) + { + Id = report.Id; + IssuerId = report.IssuerId; + TargetId = report.TargetId; + TargetOwnerId = report.TargetOwnerId; + ContextType = report.ContextType.ToString(); + Category = report.Category.ToString(); + Reason = report.Reason; + State = report.State.ToString(); + CreatedAt = report.CreatedAt; + UpdatedAt = report.UpdatedAt; + ReviewerId = report.ReviewerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/SortDto.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/SortDto.cs new file mode 100644 index 000000000..c6a5d9680 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/DTO/SortDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MiniSpace.Services.Reports.Application.DTO +{ + public class SortDto + { + public IEnumerable SortBy { get; set; } + public string Direction { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/CommentCreated.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/CommentCreated.cs new file mode 100644 index 000000000..c10c6a5e1 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/CommentCreated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reports.Application.Events.External +{ + [Message("comments")] + public class CommentCreated : IEvent + { + public Guid CommentId { get; } + + public CommentCreated(Guid commentId) + { + CommentId = commentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/EventCreated.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/EventCreated.cs new file mode 100644 index 000000000..0945bf7c1 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/EventCreated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reports.Application.Events.External +{ + [Message("events")] + public class EventCreated : IEvent + { + public Guid EventId { get; } + + public EventCreated(Guid eventId) + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/CommentCreatedHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/CommentCreatedHandler.cs new file mode 100644 index 000000000..fcf2834f9 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/CommentCreatedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Events.External.Handlers +{ + public class CommentCreatedHandler : IEventHandler + { + private readonly ICommentRepository _commentRepository; + + public CommentCreatedHandler(ICommentRepository commentRepository) + { + _commentRepository = commentRepository; + } + + public async Task HandleAsync(CommentCreated @event, CancellationToken cancellationToken) + { + if (await _commentRepository.ExistsAsync(@event.CommentId)) + { + throw new CommentAlreadyAddedException(@event.CommentId); + } + + await _commentRepository.AddAsync(new Comment(@event.CommentId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 000000000..2ca301504 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/EventCreatedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Events.External.Handlers +{ + public class EventCreatedHandler : IEventHandler + { + private readonly IEventRepository _eventRepository; + + public EventCreatedHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(EventCreated @event, CancellationToken cancellationToken = default) + { + if (await _eventRepository.ExistsAsync(@event.EventId)) + { + throw new EventAlreadyAddedException(@event.EventId); + } + + await _eventRepository.AddAsync(new Event(@event.EventId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/PostCreatedHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/PostCreatedHandler.cs new file mode 100644 index 000000000..e6e793880 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/PostCreatedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Events.External.Handlers +{ + public class PostCreatedHandler : IEventHandler + { + private readonly IPostRepository _postRepository; + + public PostCreatedHandler(IPostRepository postRepository) + { + _postRepository = postRepository; + } + + public async Task HandleAsync(PostCreated @event, CancellationToken cancellationToken) + { + if (await _postRepository.ExistsAsync(@event.PostId)) + { + throw new PostAlreadyAddedException(@event.PostId); + } + + await _postRepository.AddAsync(new Post(@event.PostId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 000000000..a286aded7 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; + +namespace MiniSpace.Services.Reports.Application.Events.External.Handlers +{ + public class StudentCreatedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + + public StudentCreatedHandler(IStudentRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task HandleAsync(StudentCreated @event, CancellationToken cancellationToken) + { + if (await _studentRepository.ExistsAsync(@event.StudentId)) + { + throw new StudentAlreadyAddedException(@event.StudentId); + } + + await _studentRepository.AddAsync(new Student(@event.StudentId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/PostCreated.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/PostCreated.cs new file mode 100644 index 000000000..a94c9a727 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/PostCreated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reports.Application.Events.External +{ + [Message("posts")] + public class PostCreated : IEvent + { + public Guid PostId { get; } + + public PostCreated(Guid postId) + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/StudentCreated.cs new file mode 100644 index 000000000..2cbad872e --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/External/StudentCreated.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reports.Application.Events.External +{ + [Message("students")] + public class StudentCreated : IEvent + { + public Guid StudentId { get; } + public string Name { get; } + + public StudentCreated(Guid studentId, string name) + { + StudentId = studentId; + Name = name; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCancelled.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCancelled.cs new file mode 100644 index 000000000..5724fac59 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCancelled.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportCancelled : IEvent + { + public Guid ReportId { get; } + + public ReportCancelled(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCreated.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCreated.cs new file mode 100644 index 000000000..f4921c1d6 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportCreated.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportCreated: IEvent + { + public Guid ReportId { get; } + + public ReportCreated(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportDeleted.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportDeleted.cs new file mode 100644 index 000000000..ccf717e3c --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportDeleted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportDeleted: IEvent + { + public Guid ReportId { get; } + + public ReportDeleted(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportRejected.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportRejected.cs new file mode 100644 index 000000000..ebab2d802 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportRejected.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportRejected: IEvent + { + public Guid ReportId { get; } + + public ReportRejected(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportResolved.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportResolved.cs new file mode 100644 index 000000000..53b383e86 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportResolved.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportResolved: IEvent + { + public Guid ReportId { get; } + + public ReportResolved(Guid reportId) + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportReviewStarted.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportReviewStarted.cs new file mode 100644 index 000000000..5971d6c04 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Events/ReportReviewStarted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Events +{ + public class ReportReviewStarted : IEvent + { + public Guid ReportId { get; } + public Guid ReviewerId { get; } + + public ReportReviewStarted(Guid reportId, Guid reviewerId) + { + ReportId = reportId; + ReviewerId = reviewerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/AppException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..4173a0478 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/CommentAlreadyAddedException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/CommentAlreadyAddedException.cs new file mode 100644 index 000000000..4bd20399e --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/CommentAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class CommentAlreadyAddedException : AppException + { + public override string Code { get; } = "comment_already_added"; + public Guid CommentId { get; } + + public CommentAlreadyAddedException(Guid commentId) + : base($"Comment with id: {commentId} was already added.") + { + CommentId = commentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/EventAlreadyAddedException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/EventAlreadyAddedException.cs new file mode 100644 index 000000000..c525c7786 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/EventAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class EventAlreadyAddedException : AppException + { + public override string Code { get; } = "event_already_added"; + public Guid EventId { get; } + + public EventAlreadyAddedException(Guid eventId) + : base($"Event with id: {eventId} was already added.") + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidContextTypeException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidContextTypeException.cs new file mode 100644 index 000000000..1ed00c27a --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidContextTypeException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class InvalidContextTypeException : AppException + { + public override string Code { get; } = "invalid_context_type"; + public string ContextType { get; } + + public InvalidContextTypeException(string contextType) : base($"Invalid context type: {contextType}.") + { + ContextType = contextType; + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReasonException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReasonException.cs new file mode 100644 index 000000000..c60091ca6 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReasonException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class InvalidReasonException : AppException + { + public override string Code { get; } = "invalid_reason"; + public string Reason { get; } + + public InvalidReasonException(string reason) : base($"Invalid reason: {reason}. It cannot be empty or over 1000 characters.") + { + Reason = reason; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportCategoryException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportCategoryException.cs new file mode 100644 index 000000000..142f32fef --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportCategoryException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class InvalidReportCategoryException : AppException + { + public override string Code { get; } = "invalid_report_category"; + public string Category { get; } + + public InvalidReportCategoryException(string category) : base($"Invalid report category: {category}.") + { + Category = category; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportState.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportState.cs new file mode 100644 index 000000000..cce794f41 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/InvalidReportState.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class InvalidReportStateException : AppException + { + public override string Code { get; } = "invalid_report_state"; + public string State { get; } + + public InvalidReportStateException(string state) : base($"Invalid report state: {state}.") + { + State = state; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/PostAlreadyAddedException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/PostAlreadyAddedException.cs new file mode 100644 index 000000000..e13dd0391 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/PostAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class PostAlreadyAddedException : AppException + { + public override string Code { get; } = "post_already_added"; + public Guid PostId { get; } + + public PostAlreadyAddedException(Guid postId) + : base($"Post with id: {postId} was already added.") + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/ReportNotFoundException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/ReportNotFoundException.cs new file mode 100644 index 000000000..c9472e2a7 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/ReportNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class ReportNotFoundException : AppException + { + public override string Code { get; } = "report_not_found"; + public Guid ReportId { get; } + + public ReportNotFoundException(Guid reportId) : base($"Report with ID: '{reportId}' was not found.") + { + ReportId = reportId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentAlreadyAddedException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentAlreadyAddedException.cs new file mode 100644 index 000000000..bd2150767 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentAlreadyAddedException.cs @@ -0,0 +1,17 @@ +using System; +using MiniSpace.Services.Reports.Core.Exceptions; + +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class StudentAlreadyAddedException : AppException + { + public override string Code { get; } = "student_already_added"; + public Guid StudentId { get; } + + public StudentAlreadyAddedException(Guid studentId) + : base($"Student with id: {studentId} was already added.") + { + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentNotFoundException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentNotFoundException.cs new file mode 100644 index 000000000..1a2fd20ec --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class StudentNotFoundException : AppException + { + public override string Code { get; } = "student_not_found"; + public Guid StudentId { get; } + + public StudentNotFoundException(Guid studentId) : base($"Student with ID: '{studentId}' was not found.") + { + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentTooManyActiveReportsException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentTooManyActiveReportsException.cs new file mode 100644 index 000000000..b40e1462d --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/StudentTooManyActiveReportsException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class StudentTooManyActiveReportsException : AppException + { + public override string Code { get; } = "student_too_many_active_reports"; + public int ActiveReports { get; } + public int MaxActiveReports { get; } + + public StudentTooManyActiveReportsException(int activeReports, int maxActiveReports) + : base($"Student has too many active reports: {activeReports}. Max allowed is: {maxActiveReports}.") + { + ActiveReports = activeReports; + MaxActiveReports = maxActiveReports; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportAccessAttemptException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportAccessAttemptException.cs new file mode 100644 index 000000000..0f7eb6e1a --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportAccessAttemptException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class UnauthorizedReportAccessAttemptException : AppException + { + public override string Code => "unauthorized_report_access_attempt"; + public Guid ReportId { get; } + public Guid StudentId { get; } + + public UnauthorizedReportAccessAttemptException(Guid reportId, Guid studentId) + : base($"Unauthorized report access attempt with ID: {reportId} by student with ID: {studentId}.") + { + ReportId = reportId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportCreationAttemptException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportCreationAttemptException.cs new file mode 100644 index 000000000..b2fa891bb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportCreationAttemptException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class UnauthorizedReportCreationAttemptException : AppException + { + public override string Code { get; } = "unauthorized_report_creation_attempt"; + public Guid StudentId { get; } + public Guid IssuerId { get; } + + public UnauthorizedReportCreationAttemptException(Guid studentId, Guid issuerId) + : base($"Unauthorized report creation attempt for student with ID: {studentId}. Issuer ID: {issuerId}.") + { + StudentId = studentId; + IssuerId = issuerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportSearchAttemptException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportSearchAttemptException.cs new file mode 100644 index 000000000..ecf336cd8 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Exceptions/UnauthorizedReportSearchAttemptException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Reports.Application.Exceptions +{ + public class UnauthorizedReportSearchAttemptException : AppException + { + public override string Code { get; } = "unauthorized_report_search_attempt"; + public Guid UserId { get; } + public string Role { get; } + + public UnauthorizedReportSearchAttemptException(Guid userId, string role) + : base($"Unauthorized report search attempt by user with ID: '{userId}' and role: {role}.") + { + UserId = userId; + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Extensions.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Extensions.cs new file mode 100644 index 000000000..790a11250 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IAppContext.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IAppContext.cs new file mode 100644 index 000000000..297000a52 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Reports.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IIdentityContext.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IIdentityContext.cs new file mode 100644 index 000000000..49b00c27d --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Reports.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/MiniSpace.Services.Reports.Application.csproj b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/MiniSpace.Services.Reports.Application.csproj new file mode 100644 index 000000000..f6e6a64af --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/MiniSpace.Services.Reports.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Queries/GetStudentReports.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Queries/GetStudentReports.cs new file mode 100644 index 000000000..66ce81dab --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Queries/GetStudentReports.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Core.Wrappers; + +namespace MiniSpace.Services.Reports.Application.Queries +{ + public class GetStudentReports : IQuery>> + { + public Guid StudentId { get; set; } + public int Page { get; set; } + public int Results { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..7fd1b751d --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reports.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IEventMapper.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..3d420a78f --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Core.Events; + +namespace MiniSpace.Services.Reports.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..7db19df2f --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reports.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportValidator.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportValidator.cs new file mode 100644 index 000000000..73f193b00 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportValidator.cs @@ -0,0 +1,13 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Application.Services +{ + public interface IReportValidator + { + ContextType ParseContextType(string contextType); + ReportCategory ParseCategory(string category); + ReportState ParseStatus(string status); + void ValidateActiveReports(int activeReports); + void ValidateReason(string reason); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportsService.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportsService.cs new file mode 100644 index 000000000..bc97204a2 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Application/Services/IReportsService.cs @@ -0,0 +1,11 @@ +using MiniSpace.Services.Reports.Application.Commands; +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Core.Wrappers; + +namespace MiniSpace.Services.Reports.Application.Services +{ + public interface IReportsService + { + Task>> BrowseReportsAsync(SearchReports command); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateId.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..a91e04b3f --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Reports.Core.Exceptions; + +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..476bd1688 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Reports.Core.Events; + +namespace MiniSpace.Services.Reports.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Comment.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Comment.cs new file mode 100644 index 000000000..e0ca38cd9 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Comment.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class Comment + { + public Guid Id { get; private set; } + + public Comment(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ContextType.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ContextType.cs new file mode 100644 index 000000000..56818d8c3 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ContextType.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public enum ContextType + { + Event, + Post, + Comment, + StudentProfile + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Event.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Event.cs new file mode 100644 index 000000000..3a404ef5c --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Event.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class Event + { + public Guid Id { get; private set; } + + public Event(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Post.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Post.cs new file mode 100644 index 000000000..08ebcde28 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Post.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class Post + { + public Guid Id; + + public Post(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Report.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Report.cs new file mode 100644 index 000000000..f74a9b65a --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Report.cs @@ -0,0 +1,81 @@ +using MiniSpace.Services.Reports.Core.Exceptions; + +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class Report : AggregateRoot + { + public Guid IssuerId { get; private set; } + public Guid TargetId { get; private set; } + public Guid TargetOwnerId { get; private set; } + public ContextType ContextType { get; private set; } + public ReportCategory Category { get; private set; } + public string Reason { get; private set; } + public ReportState State { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime UpdatedAt { get; private set; } + public Guid? ReviewerId { get; private set; } + + public Report(Guid id, Guid issuerId, Guid targetId, Guid targetOwnerId, ContextType contextType, + ReportCategory category, string reason, ReportState state, DateTime createdAt, DateTime updatedAt, + Guid? reviewerId = null) + { + Id = id; + IssuerId = issuerId; + TargetId = targetId; + TargetOwnerId = targetOwnerId; + ContextType = contextType; + Category = category; + Reason = reason; + State = state; + CreatedAt = createdAt; + UpdatedAt = updatedAt; + ReviewerId = reviewerId; + } + + public static Report Create(Guid id, Guid issuerId, Guid targetId, Guid targetOwnerId, ContextType contextType, + ReportCategory category, string reason, DateTime now) + => new Report(id, issuerId, targetId, targetOwnerId, contextType, category, reason, ReportState.Submitted, + now, now); + + public void Cancel(DateTime now) + { + if (State != ReportState.Submitted) + { + throw new InvalidReportStateException(Id, ReportState.Submitted, State); + } + State = ReportState.Cancelled; + UpdatedAt = now; + } + + public void StartReview(Guid reviewerId, DateTime now) + { + if (State != ReportState.Submitted) + { + throw new InvalidReportStateException(Id, ReportState.Submitted, State); + } + State = ReportState.UnderReview; + UpdatedAt = now; + ReviewerId = reviewerId; + } + + public void Resolve(DateTime now) + { + if (State != ReportState.UnderReview) + { + throw new InvalidReportStateException(Id, ReportState.UnderReview, State); + } + State = ReportState.Resolved; + UpdatedAt = now; + } + + public void Reject(DateTime now) + { + if (State != ReportState.UnderReview) + { + throw new InvalidReportStateException(Id, ReportState.UnderReview, State); + } + State = ReportState.Rejected; + UpdatedAt = now; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportCategory.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportCategory.cs new file mode 100644 index 000000000..bca40922f --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportCategory.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public enum ReportCategory + { + Spam, + HarassmentAndBullying, + Violence, + SexualContent, + Misinformation, + PrivacyViolations, + IntellectualPropertyViolations, + OtherViolations + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportState.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportState.cs new file mode 100644 index 000000000..e3ec3ab5c --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/ReportState.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reports.Core.Entities +{ + public enum ReportState + { + Submitted, + UnderReview, + Resolved, + Rejected, + Cancelled + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Student.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Student.cs new file mode 100644 index 000000000..daa9eaad5 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Entities/Student.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Services.Reports.Core.Entities +{ + public class Student(Guid id) + { + public Guid Id { get; private set; } = id; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..4a7fc174d --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reports.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..32243d0bb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reports.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..a5a786dab --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reports.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidReportStateException.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidReportStateException.cs new file mode 100644 index 000000000..688ea16a5 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Exceptions/InvalidReportStateException.cs @@ -0,0 +1,20 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Exceptions +{ + public class InvalidReportStateException : DomainException + { + public override string Code { get; } = "invalid_report_state"; + public Guid ReportId { get; } + public ReportState RequiredState { get; } + public ReportState CurrentState { get; } + + public InvalidReportStateException(Guid reportId, ReportState requiredState, ReportState currentState) + : base($"Report with id: {reportId} has invalid state: {currentState}. Required state: {requiredState}") + { + ReportId = reportId; + RequiredState = requiredState; + CurrentState = currentState; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/MiniSpace.Services.Reports.Core.csproj b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/MiniSpace.Services.Reports.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/MiniSpace.Services.Reports.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/ICommentRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/ICommentRepository.cs new file mode 100644 index 000000000..d8b4dd7aa --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/ICommentRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Repositories +{ + public interface ICommentRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Comment comment); + Task DeleteAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IEventRepository.cs new file mode 100644 index 000000000..d254eeb28 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IEventRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Repositories +{ + public interface IEventRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Event @event); + Task DeleteAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IPostRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IPostRepository.cs new file mode 100644 index 000000000..4c6b62e58 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IPostRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Repositories +{ + public interface IPostRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Post post); + Task DeleteAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IReportRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IReportRepository.cs new file mode 100644 index 000000000..a9bdf907b --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IReportRepository.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Repositories +{ + public interface IReportRepository + { + Task GetAsync(Guid id); + Task> GetStudentActiveReportsAsync(Guid studentId); + Task AddAsync(Report report); + Task UpdateAsync(Report report); + Task DeleteAsync(Guid id); + Task<(IEnumerable reports, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseReportsAsync( + int pageNumber, int pageSize, IEnumerable contextTypes, IEnumerable states, + Guid reviewerId, IEnumerable sortBy, string direction); + Task<(IEnumerable reports, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseStudentReportsAsync( + int pageNumber, int pageSize, Guid studentId, IEnumerable sortBy, string direction); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..fb1a98fd0 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Core.Repositories +{ + public interface IStudentRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Student student); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..61d33a008 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Reports.Core.Wrappers +{ + public class PagedResponse : Response + { + public int TotalPages { get; } + public int TotalElements { get; } + public int Size { get; } + public int Number { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + + public PagedResponse(T content, int pageNumber, int pageSize, int totalPages, int totalElements) + { + Content = content; + TotalPages = totalPages; + TotalElements = totalElements; + Size = pageSize; + Number = pageNumber; + First = pageNumber == 0; + Last = pageNumber == totalPages - 1; + Empty = totalElements == 0; + Succeeded = true; + Errors = null; + Message = null; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/Response.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/Response.cs new file mode 100644 index 000000000..7955b1131 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Core/Wrappers/Response.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Reports.Core.Wrappers +{ + public class Response + { + public T Content { get; set; } + public bool Succeeded { get; set; } + public string[] Errors { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..a6d9620a8 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Reports.Application; + +namespace MiniSpace.Services.Reports.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..30907d2eb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Reports.Application; + +namespace MiniSpace.Services.Reports.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..293495899 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Reports.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..f8e7a308e --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Reports.Application; + +namespace MiniSpace.Services.Reports.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public bool IsOrganizer { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..3400f96a2 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..6687e433e --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..7c568efe0 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,19 @@ +using Convey.MessageBrokers.RabbitMQ; +// using MiniSpace.Services.Reports.Application.Commands; +// using MiniSpace.Services.Reports.Application.Events.Rejected; +// using MiniSpace.Services.Reports.Application.Events.External; +// using MiniSpace.Services.Reports.Application.Exceptions; +// using MiniSpace.Services.Reports.Core; + +namespace MiniSpace.Services.Reports.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + _ => null + }; + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..d40597dfb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Core.Exceptions; + +namespace MiniSpace.Services.Reports.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Extensions.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Extensions.cs new file mode 100644 index 000000000..f07933ed6 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Extensions.cs @@ -0,0 +1,142 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.Reports.Application; +using MiniSpace.Services.Reports.Application.Commands; +using MiniSpace.Services.Reports.Application.Events.External; +using MiniSpace.Services.Reports.Application.Events.External.Handlers; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Contexts; +using MiniSpace.Services.Reports.Infrastructure.Decorators; +using MiniSpace.Services.Reports.Infrastructure.Exceptions; +using MiniSpace.Services.Reports.Infrastructure.Logging; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Services; + +namespace MiniSpace.Services.Reports.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("reports") + .AddMongoRepository("events") + .AddMongoRepository("posts") + .AddMongoRepository("comments") + .AddMongoRepository("students") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); + + return app; + } + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..4fc73f0ee --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Reports.Application; + +namespace MiniSpace.Services.Reports.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..7efe1fbab --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Reports.Application.Commands; + +namespace MiniSpace.Services.Reports.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(CreateReport).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..b72c79ceb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,56 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Reports.Application.Commands; +using MiniSpace.Services.Reports.Application.Events.External; + +namespace MiniSpace.Services.Reports.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(CreateReport), new HandlerLogTemplate + { + After = "Created a new report with ID: {ReportId}." + } + }, + { + typeof(CancelReport), new HandlerLogTemplate + { + After = "Canceled a report with ID: {ReportId}." + } + }, + { + typeof(DeleteReport), new HandlerLogTemplate + { + After = "Deleted a report with ID: {ReportId}." + } + }, + { + typeof(StartReportReview), new HandlerLogTemplate + { + After = "Started a review for a report with ID: {ReportId}." + } + }, + { + typeof(ResolveReport), new HandlerLogTemplate + { + After = "Resolved a report with ID: {ReportId}." + } + }, + { + typeof(RejectReport), new HandlerLogTemplate + { + After = "Rejected a report with ID: {ReportId}." + } + }, + }; + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var key = message.GetType(); + return MessageTemplates.TryGetValue(key, out var template) ? template : null; + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/MiniSpace.Services.Reports.Infrastructure.csproj b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/MiniSpace.Services.Reports.Infrastructure.csproj new file mode 100644 index 000000000..e116eb0bb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/MiniSpace.Services.Reports.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/CommentDocument.cs new file mode 100644 index 000000000..b27bbd5db --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public class CommentDocument: IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/EventDocument.cs new file mode 100644 index 000000000..5f8363c13 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/EventDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public class EventDocument: IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..70953c80d --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,81 @@ +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Report AsEntity(this ReportDocument document) + => new Report(document.Id, document.IssuerId, document.TargetId, document.TargetOwnerId, + document.ContextType, document.Category, document.Reason, document.State, document.CreatedAt, + document.UpdatedAt, document.ReviewerId); + + public static ReportDocument AsDocument(this Report entity) + => new ReportDocument() + { + Id = entity.Id, + IssuerId = entity.IssuerId, + TargetId = entity.TargetId, + TargetOwnerId = entity.TargetOwnerId, + ContextType = entity.ContextType, + Category = entity.Category, + Reason = entity.Reason, + State = entity.State, + CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, + ReviewerId = entity.ReviewerId + }; + + public static ReportDto AsDto(this ReportDocument document) + => new ReportDto() + { + Id = document.Id, + IssuerId = document.IssuerId, + TargetId = document.TargetId, + TargetOwnerId = document.TargetOwnerId, + ContextType = document.ContextType.ToString(), + Category = document.Category.ToString(), + Reason = document.Reason, + State = document.State.ToString(), + CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt, + ReviewerId = document.ReviewerId + }; + + public static Event AsEntity(this EventDocument document) + => new Event(document.Id); + + public static EventDocument AsDocument(this Event entity) + => new EventDocument + { + Id = entity.Id, + }; + + public static Post AsEntity(this PostDocument document) + => new Post(document.Id); + + public static PostDocument AsDocument(this Post entity) + => new PostDocument() + { + Id = entity.Id, + }; + + public static Comment AsEntity(this CommentDocument document) + => new Comment(document.Id); + + public static CommentDocument AsDocument(this Comment entity) + => new CommentDocument() + { + Id = entity.Id, + }; + + public static StudentDocument AsDocument(this Student entity) + => new () + { + Id = entity.Id, + }; + + public static Student AsEntity(this StudentDocument document) + => new (document.Id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/PostDocument.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/PostDocument.cs new file mode 100644 index 000000000..1db0c2a6c --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/PostDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public class PostDocument: IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/ReportDocument.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/ReportDocument.cs new file mode 100644 index 000000000..b198c1d59 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/ReportDocument.cs @@ -0,0 +1,20 @@ +using Convey.Types; +using MiniSpace.Services.Reports.Core.Entities; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public class ReportDocument: IIdentifiable + { + public Guid Id { get; set; } + public Guid IssuerId { get; set; } + public Guid TargetId { get; set; } + public Guid TargetOwnerId { get; set; } + public ContextType ContextType { get; set; } + public ReportCategory Category { get; set; } + public string Reason { get; set; } + public ReportState State { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public Guid? ReviewerId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..e910924f8 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,10 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Documents +{ + public class StudentDocument: IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Queries/Handlers/GetStudentReportsHandler.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Queries/Handlers/GetStudentReportsHandler.cs new file mode 100644 index 000000000..5af0f0339 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Queries/Handlers/GetStudentReportsHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Reports.Application; +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Application.Queries; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Core.Wrappers; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentReportsHandler : IQueryHandler>> + { + private readonly IReportRepository _reportRepository; + private readonly IAppContext _appContext; + + public GetStudentReportsHandler(IReportRepository reportRepository, IAppContext appContext) + { + _reportRepository = reportRepository; + _appContext = appContext; + } + + public async Task>> HandleAsync(GetStudentReports query, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != query.StudentId) + { + return new PagedResponse>(Enumerable.Empty(), + 1, query.Results, 0, 0); + } + + var result = await _reportRepository.BrowseStudentReportsAsync(query.Page, query.Results, + query.StudentId, Enumerable.Empty(), "dsc"); + + return new PagedResponse>(result.reports.Select(r => new ReportDto(r)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs new file mode 100644 index 000000000..cc4f1b775 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs @@ -0,0 +1,33 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public class CommentMongoRepository : ICommentRepository + { + private readonly IMongoRepository _repository; + + public CommentMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var comment = await _repository.GetAsync(c => c.Id == id); + + return comment?.AsEntity(); + } + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(c => c.Id == id); + + public Task AddAsync(Comment comment) + => _repository.AddAsync(comment.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/EventMongoRepository.cs new file mode 100644 index 000000000..05dd8abab --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -0,0 +1,33 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public class EventMongoRepository : IEventRepository + { + private readonly IMongoRepository _repository; + + public EventMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var @event = await _repository.GetAsync(e => e.Id == id); + + return @event?.AsEntity(); + } + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(e => e.Id == id); + + public Task AddAsync(Event @event) + => _repository.AddAsync(@event.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/Extensions.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/Extensions.cs new file mode 100644 index 000000000..12dfc1689 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/Extensions.cs @@ -0,0 +1,117 @@ +using System.Collections; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public static class Extensions + { + private static readonly FilterDefinitionBuilder FilterDefinitionBuilder = Builders.Filter; + public static async Task<(int totalPages, int totalElements, IReadOnlyList data)> AggregateByPage( + this IMongoCollection collection, + FilterDefinition filterDefinition, + SortDefinition sortDefinition, + int page, + int pageSize) + { + var countFacet = AggregateFacet.Create("count", + PipelineDefinition.Create(new[] + { + PipelineStageDefinitionBuilder.Count() + })); + + var dataFacet = AggregateFacet.Create("data", + PipelineDefinition.Create(new[] + { + PipelineStageDefinitionBuilder.Sort(sortDefinition), + PipelineStageDefinitionBuilder.Skip((page - 1) * pageSize), + PipelineStageDefinitionBuilder.Limit(pageSize), + })); + + + var aggregation = await collection.Aggregate() + .Match(filterDefinition) + .Facet(countFacet, dataFacet) + .ToListAsync(); + + var count = aggregation.First() + .Facets.First(x => x.Name == "count") + .Output() + ?.FirstOrDefault() + ?.Count; + + if (count == null) + { + return (0, 0, Array.Empty()); + } + var totalPages = (int)Math.Ceiling((double)count / pageSize); + + var data = aggregation.First() + .Facets.First(x => x.Name == "data") + .Output(); + + return (totalPages, (int)count, data); + } + + public static FilterDefinition ToFilterDefinition() + { + var filterDefinition = FilterDefinitionBuilder.Empty; + + return filterDefinition; + } + + public static FilterDefinition AddContextTypesFilter (this FilterDefinition filterDefinition, + IEnumerable contextTypesEnumerable) + { + var contextTypes = contextTypesEnumerable.ToList(); + if(contextTypes.Any()) + { + filterDefinition &= FilterDefinitionBuilder.In(x => x.ContextType, contextTypes); + } + return filterDefinition; + } + + public static FilterDefinition AddStatesFilter (this FilterDefinition filterDefinition, + IEnumerable statesEnumerable) + { + var states = statesEnumerable.ToList(); + if(states.Count != 0) + { + filterDefinition &= FilterDefinitionBuilder.In(x => x.State, states); + } + return filterDefinition; + } + + public static FilterDefinition AddReviewerIdFilter(this FilterDefinition filterDefinition, Guid reviewerId) + { + if (reviewerId != Guid.Empty) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.ReviewerId, reviewerId); + } + return filterDefinition; + } + + public static FilterDefinition AddStudentIdFilter(this FilterDefinition filterDefinition, Guid studentId) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.IssuerId, studentId); + return filterDefinition; + } + + public static SortDefinition ToSortDefinition(IEnumerable sortByArguments, string direction) + { + var sort = sortByArguments.ToList(); + if(sort.Count == 0) + { + sort.Add("UpdatedAt"); + } + var sortDefinitionBuilder = Builders.Sort; + var sortDefinition = sort + .Select(sortBy => direction == "asc" + ? sortDefinitionBuilder.Ascending(sortBy) + : sortDefinitionBuilder.Descending(sortBy)); + var sortCombined = sortDefinitionBuilder.Combine(sortDefinition); + return sortCombined; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/PostMongoRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/PostMongoRepository.cs new file mode 100644 index 000000000..d2658e569 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/PostMongoRepository.cs @@ -0,0 +1,33 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public class PostMongoRepository : IPostRepository + { + private readonly IMongoRepository _repository; + + public PostMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var post = await _repository.GetAsync(p => p.Id == id); + + return post?.AsEntity(); + } + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(p => p.Id == id); + + public Task AddAsync(Post post) + => _repository.AddAsync(post.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/ReportMongoRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/ReportMongoRepository.cs new file mode 100644 index 000000000..e112ecd86 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/ReportMongoRepository.cs @@ -0,0 +1,84 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public class ReportMongoRepository : IReportRepository + { + private readonly IMongoRepository _repository; + + public ReportMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var report = await _repository.GetAsync(r => r.Id == id); + + return report?.AsEntity(); + } + + public async Task> GetStudentActiveReportsAsync(Guid studentId) + { + var reports = await _repository.FindAsync(r => r.IssuerId == studentId + && r.State == ReportState.Submitted || r.State == ReportState.UnderReview); + + return reports.Select(r => r.AsEntity()); + } + + public Task AddAsync(Report report) + => _repository.AddAsync(report.AsDocument()); + + public Task UpdateAsync(Report report) + => _repository.UpdateAsync(report.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + + private async Task<(int totalPages, int totalElements, IEnumerable data)> BrowseAsync( + FilterDefinition filterDefinition, SortDefinition sortDefinition, + int pageNumber, int pageSize) + { + var pagedEvents = await _repository.Collection.AggregateByPage( + filterDefinition, + sortDefinition, + pageNumber, + pageSize); + + return pagedEvents; + } + + public async Task<(IEnumerable reports, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseReportsAsync(int pageNumber, int pageSize, + IEnumerable contextTypes, IEnumerable states, Guid reviewerId, + IEnumerable sortBy, string direction) + { + var filterDefinition = Extensions.ToFilterDefinition() + .AddContextTypesFilter(contextTypes) + .AddStatesFilter(states) + .AddReviewerIdFilter(reviewerId); + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); + + return (pagedEvents.data.Select(r => r.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); + } + + public async Task<(IEnumerable reports, int pageNumber, int pageSize, int totalPages, int totalElements)> BrowseStudentReportsAsync(int pageNumber, int pageSize, + Guid studentId, IEnumerable sortBy, string direction) + { + var filterDefinition = Extensions.ToFilterDefinition() + .AddStudentIdFilter(studentId); + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); + + return (pagedEvents.data.Select(r => r.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 000000000..2fade5d17 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reports.Infrastructure.Mongo.Repositories +{ + public class StudentMongoRepository : IStudentRepository + { + private readonly IMongoRepository _repository; + + public StudentMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + public async Task GetAsync(Guid id) + { + var student = await _repository.GetAsync(s => s.Id == id); + + return student?.AsEntity(); + } + public Task ExistsAsync(Guid id) => _repository.ExistsAsync(s => s.Id == id); + public Task AddAsync(Student student) => _repository.AddAsync(student.AsDocument()); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..770cad0eb --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Reports.Application.Services; + +namespace MiniSpace.Services.Reports.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..f171cdbe5 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core; +using MiniSpace.Services.Reports.Core.Events; + +namespace MiniSpace.Services.Reports.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + + } + + return null; + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..a832264df --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,84 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Reports.Application.Services; + +namespace MiniSpace.Services.Reports.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportValidator.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportValidator.cs new file mode 100644 index 000000000..b33c7cb56 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportValidator.cs @@ -0,0 +1,62 @@ +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Exceptions; +using InvalidReportStateException = MiniSpace.Services.Reports.Application.Exceptions.InvalidReportStateException; + +namespace MiniSpace.Services.Reports.Infrastructure.Services +{ + public class ReportValidator : IReportValidator + { + private const int MaxActiveReports = 3; + private const int MaxReasonLength = 1000; + + public ContextType ParseContextType(string contextType) + { + if (!Enum.TryParse(contextType, true, out var parsedContextType)) + { + throw new InvalidContextTypeException(contextType); + } + + return parsedContextType; + } + + public ReportCategory ParseCategory(string category) + { + if (!Enum.TryParse(category, true, out var parsedCategory)) + { + throw new InvalidReportCategoryException(category); + } + + return parsedCategory; + } + + public ReportState ParseStatus(string status) + { + if (!Enum.TryParse(status, true, out var parsedStatus)) + { + throw new InvalidReportStateException(status); + } + + return parsedStatus; + } + + public void ValidateActiveReports(int activeReports) + { + if (activeReports >= MaxActiveReports) + { + throw new StudentTooManyActiveReportsException(activeReports, MaxActiveReports); + } + } + + public void ValidateReason(string reason) + { + if (string.IsNullOrWhiteSpace(reason) || reason.Length > MaxReasonLength) + { + throw new InvalidReasonException(reason); + } + } + + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportsService.cs b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportsService.cs new file mode 100644 index 000000000..771e6b97b --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Infrastructure/Services/ReportsService.cs @@ -0,0 +1,58 @@ +using MiniSpace.Services.Reports.Application; +using MiniSpace.Services.Reports.Application.Commands; +using MiniSpace.Services.Reports.Application.DTO; +using MiniSpace.Services.Reports.Application.Exceptions; +using MiniSpace.Services.Reports.Application.Services; +using MiniSpace.Services.Reports.Core.Entities; +using MiniSpace.Services.Reports.Core.Repositories; +using MiniSpace.Services.Reports.Core.Wrappers; + +namespace MiniSpace.Services.Reports.Infrastructure.Services +{ + public class ReportsService : IReportsService + { + private readonly IReportRepository _reportRepository; + private readonly IReportValidator _reportValidator; + private readonly IAppContext _appContext; + + public ReportsService(IReportRepository reportRepository, IReportValidator reportValidator, IAppContext appContext) + { + _reportRepository = reportRepository; + _reportValidator = reportValidator; + _appContext = appContext; + } + + public async Task>> BrowseReportsAsync(SearchReports command) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && !identity.IsAdmin) + { + throw new UnauthorizedReportSearchAttemptException(identity.Id, identity.Role); + } + + var contextTypes = new List(); + foreach (var contextType in command.ContextTypes) + { + contextTypes.Add(_reportValidator.ParseContextType(contextType)); + } + + var states = new List(); + foreach (var status in command.States) + { + states.Add(_reportValidator.ParseStatus(status)); + } + + var pageNumber = command.Pageable.Page < 1 ? 1 : command.Pageable.Page; + var pageSize = command.Pageable.Size > 10 ? 10 : command.Pageable.Size; + + var result = await _reportRepository.BrowseReportsAsync(pageNumber, pageSize, + contextTypes, states, command.ReviewerId, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); + + var pagedReports = new PagedResponse>( + result.reports.Select(r => new ReportDto(r)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + return pagedReports; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/IReportsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/IReportsService.cs new file mode 100644 index 000000000..8772f115c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/IReportsService.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Reports +{ + public interface IReportsService + { + Task>>> SearchReportsAsync( + IEnumerable contextTypes, IEnumerable states, Guid reviewerId, PageableDto pageable); + + Task> CreateReportAsync(Guid reportId, Guid issuerId, Guid targetId, Guid targetOwnerId, + string contextType, string category, string reason); + + Task DeleteReportAsync(Guid reportId); + + Task> CancelReportAsync(Guid reportId); + + Task> StartReportReviewAsync(Guid reportId, Guid reviewerId); + + Task> ResolveReportAsync(Guid reportId); + + Task> RejectReportAsync(Guid reportId); + + Task>>> GetStudentReports(Guid studentId, int page, + int results); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/ReportsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/ReportsService.cs new file mode 100644 index 000000000..0cdfaeb07 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reports/ReportsService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Data.Reports; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Reports +{ + public class ReportsService: IReportsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public ReportsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task>>> SearchReportsAsync( + IEnumerable contextTypes, IEnumerable states, Guid reviewerId, PageableDto pageable) + { + return _httpClient.PostAsync>>("reports/search", + new (contextTypes, states, reviewerId, pageable)); + } + + public Task> CreateReportAsync(Guid reportId, Guid issuerId, Guid targetId, Guid targetOwnerId, + string contextType, string category, string reason) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("reports", + new { reportId, issuerId, targetId, targetOwnerId, contextType, category, reason }); + } + + public Task DeleteReportAsync(Guid reportId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"reports/{reportId}"); + } + + public Task> CancelReportAsync(Guid reportId) + { + + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"reports/{reportId}/cancel", new { reportId}); + } + + public Task> StartReportReviewAsync(Guid reportId, Guid reviewerId) + { + + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"reports/{reportId}/start-review", + new { reportId, reviewerId }); + } + + public Task> ResolveReportAsync(Guid reportId) + { + + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"reports/{reportId}/resolve", new { reportId}); + } + + public Task> RejectReportAsync(Guid reportId) + { + + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"reports/{reportId}/reject", new { reportId}); + } + + public Task>>> GetStudentReports(Guid studentId, int page, + int results) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync>>>( + $"reports/students/{studentId}?page={page}&results={results}"); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReportCategory.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReportCategory.cs new file mode 100644 index 000000000..6eca453fe --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReportCategory.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Web.DTO.Enums +{ + public enum ReportCategory + { + Spam, + HarassmentAndBullying, + Violence, + SexualContent, + Misinformation, + PrivacyViolations, + IntellectualPropertyViolations, + OtherViolations + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReportDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReportDto.cs new file mode 100644 index 000000000..176da6ec5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReportDto.cs @@ -0,0 +1,22 @@ +using System; +using MiniSpace.Web.DTO.Enums; +using MiniSpace.Web.DTO.States; +using MiniSpace.Web.DTO.Types; + +namespace MiniSpace.Web.DTO +{ + public class ReportDto + { + public Guid Id { get; set; } + public Guid IssuerId { get; set; } + public Guid TargetId { get; set; } + public Guid TargetOwnerId { get; set; } + public ReportContextType ContextType { get; set; } + public ReportCategory Category { get; set; } + public string Reason { get; set; } + public ReportState State { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public Guid? ReviewerId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/States/ReportState.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/ReportState.cs new file mode 100644 index 000000000..fdf0da322 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/ReportState.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Web.DTO.States +{ + public enum ReportState + { + Submitted, + UnderReview, + Resolved, + Rejected, + Cancelled + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/ReportContextType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/ReportContextType.cs new file mode 100644 index 000000000..efa834691 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/ReportContextType.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Web.DTO.Types +{ + public enum ReportContextType + { + Event, + Post, + Comment, + StudentProfile + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/Reports/SearchReports.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/Reports/SearchReports.cs new file mode 100644 index 000000000..c6a05a46c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Data/Reports/SearchReports.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Data.Reports +{ + public class SearchReports + { + public IEnumerable ContextTypes { get; set; } + public IEnumerable States { get; set; } + public Guid ReviewerId { get; set; } + public PageableDto Pageable { get; set; } + + public SearchReports(IEnumerable contextTypes, IEnumerable states, Guid reviewerId, + PageableDto pageable) + { + ContextTypes = contextTypes; + States = states; + ReviewerId = reviewerId; + Pageable = pageable; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index a7ff08313..536cf2123 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -25,6 +25,7 @@ using Blazored.LocalStorage; using MiniSpace.Web.Areas.MediaFiles; using MiniSpace.Web.Areas.Reactions; +using MiniSpace.Web.Areas.Reports; namespace MiniSpace.Web { @@ -72,6 +73,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }