diff --git a/.github/workflows/azure_synchronisation.yml b/.github/workflows/azure_synchronisation.yml index f65a06526..e49e4980f 100644 --- a/.github/workflows/azure_synchronisation.yml +++ b/.github/workflows/azure_synchronisation.yml @@ -28,7 +28,8 @@ jobs: AZURE_DEVOPS_USERNAME: ${{ secrets.AZURE_DEVOPS_USERNAME }} AZURE_DEVOPS_PASSWORD: ${{ secrets.AZURE_DEVOPS_PASSWORD }} run: | - git remote add azure https://$AZURE_DEVOPS_USERNAME:$AZURE_DEVOPS_PASSWORD@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO + # git remote add azure https://$AZURE_DEVOPS_USERNAME:$AZURE_DEVOPS_PASSWORD@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO + git remote add azure https://$AZURE_DEVOPS_PAT@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO - name: Push all branches to Azure DevOps run: git push azure --all --force diff --git a/.github/workflows/cloud_deploy.yml b/.github/workflows/cloud_deploy.yml index 811ab8e2e..5a289e1e8 100644 --- a/.github/workflows/cloud_deploy.yml +++ b/.github/workflows/cloud_deploy.yml @@ -3,7 +3,7 @@ name: Deploy to Cloud on: push: branches: - - main + - main # dev for the test purposes here pull_request: branches: - main @@ -21,7 +21,7 @@ jobs: echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.DROPLET_IP }} >> ~/.ssh/known_hosts - cat ~/.ssh/known_hosts # Add this line to confirm that the key is added + cat ~/.ssh/known_hosts - name: Check the structure run: | diff --git a/MiniSpace.APIGateway/Dockerfile b/MiniSpace.APIGateway/Dockerfile index dcddd84fd..11c8514cc 100644 --- a/MiniSpace.APIGateway/Dockerfile +++ b/MiniSpace.APIGateway/Dockerfile @@ -1,12 +1,17 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app + COPY . . -RUN dotnet publish src/MiniSpace.APIGateway -c release -o out -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 +RUN dotnet publish src/MiniSpace.APIGateway -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app + COPY --from=build /app/out . -ENV ASPNETCORE_URLS http://*:80 -ENV ASPNETCORE_ENVIRONMENT docker -ENV NTRADA_CONFIG ntrada.docker -ENTRYPOINT dotnet MiniSpace.APIGateway.dll \ No newline at end of file + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.APIGateway.dll"] diff --git a/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh b/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh index 8307a0b36..c4a70689f 100755 --- a/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh +++ b/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh @@ -1,6 +1,6 @@ #!/bin/bash -export ASPNETCORE_ENVIRONMENT=Development +export ASPNETCORE_ENVIRONMENT=docker cd .. diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/MiniSpace.APIGateway.csproj b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/MiniSpace.APIGateway.csproj index 954239d3c..7e786b1db 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/MiniSpace.APIGateway.csproj +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/MiniSpace.APIGateway.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 latest MiniSpace.APIGateway diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/Properties/launchSettings.json b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/Properties/launchSettings.json index 196b90ae7..c0c6c75d3 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/Properties/launchSettings.json +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/Properties/launchSettings.json @@ -14,7 +14,7 @@ "ASPNETCORE_ENVIRONMENT": "local" } }, - "Pacco.APIGateway": { + "MiniSpace.APIGateway": { "commandName": "Project", "launchBrowser": false, "applicationUrl": "http://localhost:5000", diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 10f0d3548..1134ee004 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -74,6 +74,7 @@ modules: returnValue: Welcome to MiniSpace API! + identity: path: /identity routes: @@ -106,10 +107,347 @@ modules: downstream: identity-service/sign-in auth: false - - services: identity-service: localUrl: localhost:5004 url: identity-service - \ No newline at end of file + + + + reports: + path: /reports + routes: + - upstream: / + method: POST + use: downstream + downstream: reports-service/reports + auth: true + + - upstream: / + method: GET + use: downstream + downstream: reports-service/reports + auth: true + + - upstream: / + method: PUT + use: downstream + downstream: reports-service/reports + auth: true + + - upstream: /{reportId} + method: GET + use: downstream + downstream: reports-service/reports/{reportId} + auth: true + + services: + reports-service: + localUrl: localhost:5005 + url: reports-service + + + + notifications: + path: /notifications + routes: + - upstream: / + method: POST + use: downstream + downstream: notifications-service/notifications + auth: true + + - upstream: /{userId} + method: GET + use: downstream + downstream: notifications-service/notifications/{userId} + auth: true + + services: + notifications-service: + localUrl: localhost:5006 + url: notifications-service + + + + students: + path: /students + routes: + - upstream: / + method: GET + use: downstream + downstream: students-service/students + auth: true + claims: + role: admin + + - upstream: /{studentId} + method: GET + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: /{studentId} + method: PUT + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: /{studentId} + method: DELETE + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: / + method: POST + use: downstream + downstream: students-service/students + auth: true + + - upstream: /{studentId}/state/{state} + method: PUT + use: downstream + downstream: students-service/students/{studentId}/state/{state} + auth: true + claims: + role: admin + + + services: + students-service: + localUrl: localhost:5007 + url: students-service + + + + events: + path: /events + routes: + - upstream: / + method: POST + use: downstream + downstream: events-service/events + auth: true + + - upstream: /{eventId} + method: PUT + use: downstream + downstream: events-service/events/{eventId} + auth: true + + - upstream: /{eventId} + method: GET + use: downstream + downstream: events-service/events/{eventId} + + - upstream: /{eventId} + method: DELETE + use: downstream + downstream: events-service/events/{eventId} + auth: true + + - upstream: /search + method: POST + use: downstream + downstream: events-service/events/search + + - upstream: /{eventId}/show-interest + method: POST + use: downstream + downstream: events-service/events/{eventId}/show-interest + auth: true + + - upstream: /{eventId}/sign-up + method: POST + use: downstream + downstream: events-service/events/{eventId}/sign-up + auth: true + + - upstream: /organizer/{organizerId} + method: GET + use: downstream + downstream: events-service/events/organizer/{organizerId} + auth: true + + services: + events-service: + localUrl: localhost:5008 + url: events-service + + + + comments: + path: /comments + routes: + - upstream: / + method: POST + use: downstream + downstream: comments-service/comments + auth: true + + - upstream: /{commentId} + method: PUT + use: downstream + downstream: comments-service/comments/{commentId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: comments-service/comments + auth: true + + - upstream: /{commentId} + method: DELETE + use: downstream + downstream: comments-service/comments/{commentId} + auth: true + + - upstream: /{commentId}like + method: POST + use: downstream + downstream: comments-service/comments/{commentId}/like + auth: true + + services: + comments-service: + localUrl: localhost:5009 + url: comments-service + + + + reactions: + path: /reactions + routes: + - upstream: / + method: POST + use: downstream + downstream: reactions-service/reactions + auth: true + + - upstream: / + method: DELETE + use: downstream + downstream: reactions-service/reactions + auth: true + + - upstream: / + method: GET + use: downstream + downstream: reactions-service/reactions + auth: true + + services: + reactions-service: + localUrl: localhost:5010 + url: reactions-service + + + + statistics: + path: /statistics + routes: + - upstream: / + method: GET + use: downstream + downstream: statistics-service/statistics + auth: true + + - upstream: /rating + method: GET + use: downstream + downstream: statistics-service/statistics/rating + auth: true + + - upstream: /rating + method: POST + use: downstream + downstream: statistics-service/statistics/rating + auth: true + + services: + statistics-service: + localUrl: localhost:5011 + url: statistics-service + + + + friends: + path: /friends + routes: + - upstream: / + method: GET + use: downstream + downstream: friends-service/friends + auth: true + + - upstream: /{userId} + method: POST + use: downstream + downstream: friends-service/friends/{userId} + auth: true + + - upstream: /notYet + method: GET + use: downstream + downstream: friends-service/friends/notYet + auth: true + + - upstream: /pending + method: POST + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + auth: true + + services: + friends-service: + localUrl: localhost:5012 + url: friends-service + + + + posts: + path: /posts + routes: + - upstream: / + method: POST + use: downstream + downstream: posts-service/posts + auth: true + + - upstream: /{postId} + method: PUT + use: downstream + downstream: posts-service/posts/{postId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: posts-service/posts + auth: true + + - upstream: /{postId} + method: DELETE + use: downstream + downstream: posts-service/posts/{postId} + auth: true + + + services: + posts-service: + localUrl: localhost:5013 + url: posts-service diff --git a/MiniSpace.Services.Events/.gitignore b/MiniSpace.Services.Events/.gitignore new file mode 100644 index 000000000..e263fe681 --- /dev/null +++ b/MiniSpace.Services.Events/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ \ No newline at end of file diff --git a/MiniSpace.Services.Events/Dockerfile b/MiniSpace.Services.Events/Dockerfile new file mode 100644 index 000000000..07c6d282f --- /dev/null +++ b/MiniSpace.Services.Events/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Events.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Events.Api.dll"] diff --git a/MiniSpace.Services.Events/LICENSE b/MiniSpace.Services.Events/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.Events/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.Events/MiniSpace.Services.Events.sln b/MiniSpace.Services.Events/MiniSpace.Services.Events.sln new file mode 100644 index 000000000..1d0973a8b --- /dev/null +++ b/MiniSpace.Services.Events/MiniSpace.Services.Events.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", "{9C2CAF5E-5553-4271-A630-B869E43317E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Events.Api", "src\MiniSpace.Services.Events.Api\MiniSpace.Services.Events.Api.csproj", "{36B1F678-92AA-447B-A0D3-D8FB8A88FAC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Events.Application", "src\MiniSpace.Services.Events.Application\MiniSpace.Services.Events.Application.csproj", "{870609C9-905C-4317-8CF6-EC12FF0A9419}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Events.Core", "src\MiniSpace.Services.Events.Core\MiniSpace.Services.Events.Core.csproj", "{39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Events.Infrastructure", "src\MiniSpace.Services.Events.Infrastructure\MiniSpace.Services.Events.Infrastructure.csproj", "{0E950D6A-9565-4AC9-8DA0-49C97E70D3B5}" +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 + {36B1F678-92AA-447B-A0D3-D8FB8A88FAC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36B1F678-92AA-447B-A0D3-D8FB8A88FAC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36B1F678-92AA-447B-A0D3-D8FB8A88FAC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36B1F678-92AA-447B-A0D3-D8FB8A88FAC2}.Release|Any CPU.Build.0 = Release|Any CPU + {870609C9-905C-4317-8CF6-EC12FF0A9419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {870609C9-905C-4317-8CF6-EC12FF0A9419}.Debug|Any CPU.Build.0 = Debug|Any CPU + {870609C9-905C-4317-8CF6-EC12FF0A9419}.Release|Any CPU.ActiveCfg = Release|Any CPU + {870609C9-905C-4317-8CF6-EC12FF0A9419}.Release|Any CPU.Build.0 = Release|Any CPU + {39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC}.Release|Any CPU.Build.0 = Release|Any CPU + {0E950D6A-9565-4AC9-8DA0-49C97E70D3B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E950D6A-9565-4AC9-8DA0-49C97E70D3B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E950D6A-9565-4AC9-8DA0-49C97E70D3B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E950D6A-9565-4AC9-8DA0-49C97E70D3B5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {36B1F678-92AA-447B-A0D3-D8FB8A88FAC2} = {9C2CAF5E-5553-4271-A630-B869E43317E3} + {870609C9-905C-4317-8CF6-EC12FF0A9419} = {9C2CAF5E-5553-4271-A630-B869E43317E3} + {39D9DFA6-70DC-4DCD-A993-E0F6A2E0D1CC} = {9C2CAF5E-5553-4271-A630-B869E43317E3} + {0E950D6A-9565-4AC9-8DA0-49C97E70D3B5} = {9C2CAF5E-5553-4271-A630-B869E43317E3} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Events/scripts/build.sh b/MiniSpace.Services.Events/scripts/build.sh new file mode 100644 index 000000000..c80836499 --- /dev/null +++ b/MiniSpace.Services.Events/scripts/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Events.Api +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Events/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Events/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..060238b80 --- /dev/null +++ b/MiniSpace.Services.Events/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.events:latest . + +docker tag minispace.services.events:latest adrianvsaint/minispace.services.events:latest + +docker push adrianvsaint/minispace.services.events:latest diff --git a/MiniSpace.Services.Events/scripts/start.sh b/MiniSpace.Services.Events/scripts/start.sh new file mode 100644 index 000000000..12fa688db --- /dev/null +++ b/MiniSpace.Services.Events/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Events.Api +dotnet run diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.csproj new file mode 100644 index 000000000..f7bb879b4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/MiniSpace.Services.Events.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Identity.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs new file mode 100644 index 000000000..baf7c85bc --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Secrets.Vault; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Infrastructure; + +namespace MiniSpace.Services.Identity.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 + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Post("events/search", async (cmd, ctx) => + { + var pagedResult = await ctx.RequestServices.GetService().SignInAsync(cmd); + await ctx.Response.WriteJsonAsync(pagedResult); + })) + .UseDispatcherEndpoints(endpoints => endpoints + .Get("events/{eventId}") + //.Get>("events/organizer/{organizerId}") + //.Put("events/{eventId}") + .Post("events", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) + .Post("events/{eventId}/sign-up") + .Post("events/{eventId}/show-interest") + .Post("events/{eventId}/rate") + // TODO: Add query for student latest enrolled events + .Get>>("events/student/{studentId}") + .Delete("events/{eventId}") + ) + ) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Properties/launchSettings.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Properties/launchSettings.json new file mode 100644 index 000000000..8ed7b6e74 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5008" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Events": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5008", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.Development.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.Development.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json new file mode 100644 index 000000000..13a4cdc07 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Events Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "events-service", + "address": "events-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "events-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": "events", + "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": "events-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "events-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": "events" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "events-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "events:" + }, + "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.Events/src/MiniSpace.Services.Events.Api/appsettings.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.local.json b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.local.json new file mode 100644 index 000000000..e5c8a40dc --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.local.json @@ -0,0 +1,196 @@ +{ + "app": { + "name": "MiniSpace Events Service", + "service": "events-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "events-service", + "address": "docker.for.win.localhost", + "port": "5008", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "events-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": { + "students": "http://localhost:5007" + }, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "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": "events", + "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": "events-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "events-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": "events" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "events-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "events:" + }, + "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": "events-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "events-service", + "commonName": "events-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "events-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs new file mode 100644 index 000000000..cfb4d59ae --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs @@ -0,0 +1,48 @@ +using System; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class AddEvent : ICommand + { + public Guid EventId { get; } + public string Name { get; } + public Guid OrganizerId { get; } + public string StartDate { get; } + public string EndDate { get; } + public string BuildingName { get; } + public string Street { get; } + public string BuildingNumber { get; } + public string ApartmentNumber { get; } + public string City { get; } + public string ZipCode { get; } + public string Description { get; } + public int Capacity { get; } + public decimal Fee { get; } + public string Category { get; } + public string PublishDate { get; } + + public AddEvent(Guid eventId, string name, Guid organizerId, string startDate, string endDate, + string buildingName, string street, string buildingNumber, string apartmentNumber, string city, + string zipCode, string description, int capacity, decimal fee, string category, string publishDate) + { + EventId = eventId == Guid.Empty ? Guid.NewGuid() : eventId; + Name = name; + OrganizerId = organizerId; + StartDate = startDate; + EndDate = endDate; + BuildingName = buildingName; + Street = street; + BuildingNumber = buildingNumber; + ApartmentNumber = apartmentNumber; + City = city; + ZipCode = zipCode; + Description = description; + Capacity = capacity; + Fee = fee; + Category = category; + PublishDate = publishDate; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/DeleteEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/DeleteEvent.cs new file mode 100644 index 000000000..3deb905b8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/DeleteEvent.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class DeleteEvent: ICommand + { + public Guid EventId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs new file mode 100644 index 000000000..9dfebeb32 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs @@ -0,0 +1,68 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class AddEventHandler: ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IEventValidator _eventValidator; + private readonly IAppContext _appContext; + + public AddEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, IEventMapper eventMapper, + IDateTimeProvider dateTimeProvider, IEventValidator eventValidator, IAppContext appContext) + { + _eventRepository = eventRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + _dateTimeProvider = dateTimeProvider; + _eventValidator = eventValidator; + _appContext = appContext; + } + + public async Task HandleAsync(AddEvent command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsOrganizer) + throw new AuthorizedUserIsNotAnOrganizerException(identity.Id); + if(identity.Id != command.OrganizerId) + throw new OrganizerCannotAddEventForAnotherOrganizerException(identity.Id, command.OrganizerId); + + var category = _eventValidator.ParseCategory(command.Category); + var startDate = _eventValidator.ParseDate(command.StartDate, "event_start_date"); + var endDate = _eventValidator.ParseDate(command.EndDate, "event_end_date"); + var now = _dateTimeProvider.Now; + _eventValidator.ValidateDates(now, startDate, "now", "event_start_date"); + _eventValidator.ValidateDates(startDate, endDate, "event_start_date", "event_end_date"); + + var publishDate = now; + var status = State.Published; + if (command.PublishDate != null) + { + publishDate = _eventValidator.ParseDate(command.PublishDate, "event_publish_date"); + _eventValidator.ValidateDates(now, publishDate, "now", "event_publish_date"); + status = State.ToBePublished; + } + + var address = new Address(command.BuildingName, command.Street, command.BuildingNumber, + command.ApartmentNumber, command.City, command.ZipCode); + var organizer = new Organizer(command.OrganizerId, identity.Name, identity.Email, string.Empty); + var @event = Event.Create(command.EventId, command.Name, command.Description, startDate, endDate, + address, command.Capacity, command.Fee, category, status, publishDate, organizer); + + await _eventRepository.AddAsync(@event); + await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/DeleteEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/DeleteEventHandler.cs new file mode 100644 index 000000000..bc8736389 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/DeleteEventHandler.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class DeleteEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteEventHandler(IEventRepository eventRepository, IAppContext appContext, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteEvent command, CancellationToken cancellationToken) + { + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) + { + throw new EventNotFoundException(command.EventId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != @event.Organizer.Id && !identity.IsAdmin) + { + throw new UnauthorizedEventAccessException(command.EventId, identity.Id); + } + + await _eventRepository.DeleteAsync(command.EventId); + await _messageBroker.PublishAsync(new EventDeleted(command.EventId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RateEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RateEventHandler.cs new file mode 100644 index 000000000..514e76926 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RateEventHandler.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class RateEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IStudentRepository _studentRepository; + + public RateEventHandler(IEventRepository eventRepository, IStudentRepository studentRepository) + { + _eventRepository = eventRepository; + _studentRepository = studentRepository; + } + + public async Task HandleAsync(RateEvent command, CancellationToken cancellationToken) + { + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) + { + throw new EventNotFoundException(command.EventId); + } + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + @event.Rate(command.StudentId, command.Rating); + await _eventRepository.UpdateAsync(@event); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ShowInterestInEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ShowInterestInEventHandler.cs new file mode 100644 index 000000000..559a995c2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ShowInterestInEventHandler.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class ShowInterestInEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IStudentRepository _studentRepository; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public ShowInterestInEventHandler(IEventRepository eventRepository, IStudentRepository studentRepository, + IMessageBroker messageBroker, IAppContext appContext) + { + _eventRepository = eventRepository; + _studentRepository = studentRepository; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(ShowInterestInEvent command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != command.StudentId) + { + throw new UnauthorizedEventAccessException(command.EventId, command.StudentId); + } + + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) + { + throw new EventNotFoundException(command.EventId); + } + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var participant = new Participant(student.Id, identity.Name, identity.Email); + @event.ShowStudentInterest(participant); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new StudentShowedInterestInEvent(@event.Id, student.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/SignUpToEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/SignUpToEventHandler.cs new file mode 100644 index 000000000..cc94246eb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/SignUpToEventHandler.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Exceptions; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class SignUpToEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IStudentRepository _studentRepository; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public SignUpToEventHandler(IEventRepository eventRepository, IStudentRepository studentRepository, + IMessageBroker messageBroker, IAppContext appContext) + { + _eventRepository = eventRepository; + _studentRepository = studentRepository; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(SignUpToEvent command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != command.StudentId) + { + throw new UnauthorizedEventAccessException(command.EventId, command.StudentId); + } + + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) + { + throw new EventNotFoundException(command.EventId); + } + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var participant = new Participant(student.Id, identity.Name, identity.Email); + @event.SignUpStudent(participant); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new StudentSignedUpToEvent(@event.Id, student.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventsStateHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventsStateHandler.cs new file mode 100644 index 000000000..e0139ae43 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventsStateHandler.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class UpdateEventsStateHandler : ICommandHandler + { + private IEventRepository _eventRepository; + private IMessageBroker _messageBroker; + + public UpdateEventsStateHandler(IEventRepository eventRepository, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateEventsState command, CancellationToken cancellationToken) + { + var events = (await _eventRepository.GetAllAsync()).ToList(); + foreach (var @event in events) + { + if (@event.UpdateState(command.Now)) + await _eventRepository.UpdateAsync(@event); + } + + await _messageBroker.PublishAsync(new EventsStateUpdated(command.Now)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RateEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RateEvent.cs new file mode 100644 index 000000000..d25b239e5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RateEvent.cs @@ -0,0 +1,12 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class RateEvent : ICommand + { + public Guid EventId { get; set; } + public int Rating { get; set; } + public Guid StudentId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs new file mode 100644 index 000000000..1b9ef6791 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class SearchEvents : ICommand + { + public string Name { get; set; } + public string Organizer { get; set; } + public string DateFrom { get; set; } + public string DateTo { get; set; } + public PageableDto Pageable { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ShowInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ShowInterestInEvent.cs new file mode 100644 index 000000000..ac3e05df3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ShowInterestInEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class ShowInterestInEvent: ICommand + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public ShowInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SignUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SignUpToEvent.cs new file mode 100644 index 000000000..13009860d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SignUpToEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class SignUpToEvent : ICommand + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public SignUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEventsState.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEventsState.cs new file mode 100644 index 000000000..3fa7e5669 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEventsState.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class UpdateEventsState(DateTime now) : ICommand + { + public DateTime Now { get; set; } = now; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/ContractAttribute.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/ContractAttribute.cs new file mode 100644 index 000000000..e23d3e748 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/ContractAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace MiniSpace.Services.Events.Application +{ + public class ContractAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/AddressDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/AddressDto.cs new file mode 100644 index 000000000..86edc970b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/AddressDto.cs @@ -0,0 +1,28 @@ +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class AddressDto + { + public string BuildingName { get; set; } + public string Street { get; set; } + public string BuildingNumber { get; set; } + public string ApartmentNumber { get; set; } + public string City { get; set; } + public string ZipCode { get; set; } + + public AddressDto() + { + } + + public AddressDto(Address address) + { + BuildingName = address.BuildingName; + Street = address.Street; + BuildingNumber = address.BuildingNumber; + ApartmentNumber = address.ApartmentNumber; + City = address.City; + ZipCode = address.ZipCode; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs new file mode 100644 index 000000000..0f104d13b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class EventDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public OrganizerDto Organizer { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public IEnumerable CoOrganizers { get; set; } + public AddressDto Location { get; set; } + //public string Image { get; set; } + public int InterestedStudents { get; set; } + public int SignedUpStudents { get; set; } + public int Capacity { get; set; } + public decimal Fee { get; set; } + public string Category { get; set; } + public string Status { get; set; } + public DateTime PublishDate { get; set; } + + public EventDto() + { + } + + public EventDto(Event @event) + { + Id = @event.Id; + Name = @event.Name; + Description = @event.Description; + Organizer = new OrganizerDto(@event.Organizer); + StartDate = @event.StartDate; + EndDate = @event.EndDate; + CoOrganizers = @event.CoOrganizers.Select(x => new OrganizerDto(x)); + Location = new AddressDto(@event.Location); + InterestedStudents = @event.InterestedStudents.Count(); + SignedUpStudents = @event.SignedUpStudents.Count(); + Capacity = @event.Capacity; + Fee = @event.Fee; + Category = @event.Category.ToString(); + Status = @event.State.ToString(); + PublishDate = @event.PublishDate; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizerDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizerDto.cs new file mode 100644 index 000000000..bbdb13ef7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizerDto.cs @@ -0,0 +1,25 @@ +using System; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class OrganizerDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Organization { get; set; } + + public OrganizerDto() + { + } + + public OrganizerDto(Organizer organizer) + { + Id = organizer.Id; + Name = organizer.Name; + Email = organizer.Email; + Organization = organizer.Organization; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PageableDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PageableDto.cs new file mode 100644 index 000000000..94ea595fd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PageableDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/DTO/SortDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/SortDto.cs new file mode 100644 index 000000000..eab55d0fd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/SortDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs new file mode 100644 index 000000000..6ef744d3e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class StudentDto + { + public Guid Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentEventsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentEventsDto.cs new file mode 100644 index 000000000..c160051c8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentEventsDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class StudentEventsDto + { + public Guid StudentId { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStarted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStarted.cs new file mode 100644 index 000000000..b6229b652 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStarted.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventBackgroundWorkerStarted(string name) : IEvent + { + public string Name { get; set; } = name; + } +} + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStopped.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStopped.cs new file mode 100644 index 000000000..7c7cf5095 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventBackgroundWorkerStopped.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventBackgroundWorkerStopped(string name) : IEvent + { + public string Name { get; set; } = name; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs new file mode 100644 index 000000000..0df57828b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs @@ -0,0 +1,11 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventCreated(Guid eventId, Guid organizerId) : IEvent + { + public Guid EventId { get; set; } = eventId; + public Guid OrganizerId { get; set; } = organizerId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs new file mode 100644 index 000000000..a39a19cb8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventDeleted(Guid eventId) : IEvent + { + public Guid EventId { get; set; } = eventId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventViewed.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventViewed.cs new file mode 100644 index 000000000..d6322b5ef --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventViewed.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventViewed(Guid eventId) : IEvent + { + public Guid EventId { get; set; } = eventId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventsStateUpdated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventsStateUpdated.cs new file mode 100644 index 000000000..7022d2562 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventsStateUpdated.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventsStateUpdated(DateTime updateDateTime) : IEvent + { + public DateTime UpdateDateTime { get; set; } = updateDateTime; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 000000000..d372947b9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.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.Events.Application.Exceptions; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/StudentCreated.cs new file mode 100644 index 000000000..91b9e6647 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/StudentCreated.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs new file mode 100644 index 000000000..0184bbe9d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Events; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class AddEventRejected(Guid userId, string reason, string code) : IRejectedEvent + { + public Guid UserId { get; } = userId; + public string Reason { get; } = reason; + public string Code { get; } = code; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs new file mode 100644 index 000000000..a6e51675f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentShowedInterestInEvent.cs @@ -0,0 +1,18 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class StudentShowedInterestInEvent: IEvent + + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentShowedInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs new file mode 100644 index 000000000..c7ce08244 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentSignedUpToEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class StudentSignedUpToEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentSignedUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AppException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..55b961ebc --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AppException.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public abstract class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AuthorizedUserIsNotAnOrganizerException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AuthorizedUserIsNotAnOrganizerException.cs new file mode 100644 index 000000000..52b51ef80 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/AuthorizedUserIsNotAnOrganizerException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class AuthorizedUserIsNotAnOrganizerException: AppException + { + public Guid UserId { get; } + + public AuthorizedUserIsNotAnOrganizerException(Guid userId) + : base($"User with id: '{userId}' is not an organizer") + { + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/EventNotFoundException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/EventNotFoundException.cs new file mode 100644 index 000000000..66fe48dc4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/EventNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class EventNotFoundException : AppException + { + public override string Code { get; } = "event_not_found"; + public Guid EventId { get; } + + public EventNotFoundException(Guid eventId) : base($"Event with ID: '{eventId}' was not found.") + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventCategoryException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventCategoryException.cs new file mode 100644 index 000000000..826a390eb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventCategoryException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventCategoryException : AppException + { + public override string Code { get; } = "invalid_event_category"; + public string Category { get; } + + public InvalidEventCategoryException(string category) : base($"Invalid event category: {category}") + { + Category = category; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeException.cs new file mode 100644 index 000000000..c4a8c4fca --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeException.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventDateTimeException(string element, string value) + : AppException($"Event DateTime property `{element}` is invalid: {value}.") + { + public override string Code { get; } = "invalid_event_date_time"; + public string Element { get; } = element; + public string Value { get; } = value; + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeOrderException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeOrderException.cs new file mode 100644 index 000000000..cd0323105 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDateTimeOrderException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventDateTimeOrderException(DateTime earlierDate, DateTime laterDate, string earlierDateProperty, string laterDateProperty) + : AppException($"Event DateTime property `{earlierDateProperty}`:`{earlierDate}` is later than `{laterDateProperty}`:`{laterDate}`.") + { + public override string Code { get; } = "invalid_event_date_time"; + public string EarlierProperty { get; } = earlierDateProperty; + public string LaterProperty { get; } = laterDateProperty; + public DateTime EarlierDate { get; } = earlierDate; + public DateTime LaterDate { get; } = laterDate; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerCannotAddEventForAnotherOrganizerException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerCannotAddEventForAnotherOrganizerException.cs new file mode 100644 index 000000000..e62733cc3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerCannotAddEventForAnotherOrganizerException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class OrganizerCannotAddEventForAnotherOrganizerException : AppException + { + public override string Code { get; } = "organizer_cannot_add_event_for_another_organizer"; + public Guid OrganizerId { get; } + public Guid RequestedOrganizerId { get; } + + public OrganizerCannotAddEventForAnotherOrganizerException(Guid organizerId, Guid requestedOrganizerId) + : base($"Organizer with ID: {organizerId} cannot add event for organizer with ID: {requestedOrganizerId}.") + { + OrganizerId = organizerId; + RequestedOrganizerId = requestedOrganizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentAlreadyAddedException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentAlreadyAddedException.cs new file mode 100644 index 000000000..ae9d2e321 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentAlreadyAddedException.cs @@ -0,0 +1,17 @@ +using System; +using MiniSpace.Services.Events.Core.Exceptions; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentNotFoundException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentNotFoundException.cs new file mode 100644 index 000000000..c8193e7ec --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/StudentNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedEventAccesException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedEventAccesException.cs new file mode 100644 index 000000000..64c1758c1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedEventAccesException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class UnauthorizedEventAccessException : AppException + { + public override string Code { get; } = "unauthorized_event_access"; + public Guid EventId { get; } + public Guid UserId { get; } + + public UnauthorizedEventAccessException(Guid eventId, Guid userId) + : base($"Unauthorized access to event with ID: '{eventId}' by user with ID: '{userId}'.") + { + EventId = eventId; + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Extensions.cs new file mode 100644 index 000000000..6d9d87ca3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IAppContext.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IAppContext.cs new file mode 100644 index 000000000..3b5be229b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Events.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IIdentityContext.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IIdentityContext.cs new file mode 100644 index 000000000..1e49fffdb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/IIdentityContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.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; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.csproj new file mode 100644 index 000000000..16556e065 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/MiniSpace.Services.Events.Application.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEvent.cs new file mode 100644 index 000000000..11c4a0f2b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEvent.cs @@ -0,0 +1,11 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Queries +{ + public class GetEvent : IQuery + { + public Guid EventId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs new file mode 100644 index 000000000..1f8f12315 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetStudentEvents.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries +{ + public class GetStudentEvents : IQuery>> + { + public Guid StudentId { get; set; } + public int numberOfResults { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs new file mode 100644 index 000000000..8ff924d0a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Services.Clients +{ + public interface IStudentsServiceClient + { + Task GetAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Events/EventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Events/EventService.cs new file mode 100644 index 000000000..ff8bc3d7a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Events/EventService.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Services.Events +{ + public class EventService : IEventService + { + private readonly IEventRepository _eventRepository; + private readonly IEventValidator _eventValidator; + private readonly IMessageBroker _messageBroker; + + public EventService(IEventRepository eventRepository, IEventValidator eventValidator, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _eventValidator = eventValidator; + _messageBroker = messageBroker; + } + + public async Task>> SignInAsync(SearchEvents command) + { + var dateTo = DateTime.MinValue; + var dateFrom = DateTime.MinValue; + if(command.DateTo != string.Empty) + { + _eventValidator.ParseDate(command.DateTo, "DateTo"); + } + if(command.DateFrom != string.Empty) + { + _eventValidator.ParseDate(command.DateFrom, "DateFrom"); + } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); + + var result = await _eventRepository.BrowseAsync( + pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, + command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction, State.Published); + + var pagedEvents = new PagedResponse>(result.Item1.Select(e => new EventDto(e)), + result.Item2, result.Item3, result.Item4, result.Item5); + + return pagedEvents; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..9079460bb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..b2a9e2e97 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventMapper.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Core; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs new file mode 100644 index 000000000..5bde55989 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Wrappers; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IEventService + { + Task>> SignInAsync(SearchEvents command); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs new file mode 100644 index 000000000..1ed5a713a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs @@ -0,0 +1,13 @@ +using System; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IEventValidator + { + Category ParseCategory(string categoryString); + DateTime ParseDate(string dateString, string fieldName); + void ValidateDates(DateTime earlierDate, DateTime laterDate, string earlierDateString, string endDateString); + (int pageNumber, int pageSize) PageFilter(int pageNumber, int pageSize); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..22731c6dc --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Address.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Address.cs new file mode 100644 index 000000000..6da0ed1dc --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Address.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Address( + string buildingName, + string street, + string buildingNumber, + string apartmentNumber, + string city, + string zipCode) + { + public string BuildingName { get; set; } = buildingName; + public string Street { get; set; } = street; + public string BuildingNumber { get; set; } = buildingNumber; + public string ApartmentNumber { get; set; } = apartmentNumber; + public string City { get; set; } = city; + public string ZipCode { get; set; } = zipCode; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateId.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..1013f656d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateId.cs @@ -0,0 +1,51 @@ +using System; +using MiniSpace.Services.Events.Core.Exceptions; + +namespace MiniSpace.Services.Events.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(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..190f38529 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.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(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Category.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Category.cs new file mode 100644 index 000000000..93b2d6829 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Category.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Events.Core.Entities +{ + public enum Category + { + Music, + Sports, + Education, + Science, + Technology, + Art, + Business, + Health, + Charity, + Other + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs new file mode 100644 index 000000000..b9bf64a09 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Events.Core.Exceptions; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Event: AggregateRoot + { + private ISet _coOrganizers = new HashSet(); + private ISet _interestedStudents = new HashSet(); + private ISet _signedUpStudents = new HashSet(); + private ISet _ratings = new HashSet(); + public string Name { get; private set; } + public string Description { get; private set; } + public Organizer Organizer { get; private set; } + public DateTime StartDate { get; private set; } + public DateTime EndDate { get; private set; } + public Address Location { get; private set; } + //public string Image { get; set; } + public int Capacity { get; private set; } + public decimal Fee { get; private set; } + public Category Category { get; private set; } + public State State { get; private set; } + public DateTime PublishDate { get; private set; } + + public IEnumerable CoOrganizers + { + get => _coOrganizers; + private set => _coOrganizers = new HashSet(value); + } + + public IEnumerable InterestedStudents + { + get => _interestedStudents; + private set => _interestedStudents = new HashSet(value); + } + + public IEnumerable SignedUpStudents + { + get => _signedUpStudents; + private set => _signedUpStudents = new HashSet(value); + } + + public IEnumerable Ratings + { + get => _ratings; + private set => _ratings = new HashSet(value); + } + + public Event(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, + Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, + Organizer organizer, IEnumerable coOrganizers = null, IEnumerable interestedStudents = null, + IEnumerable signedUpStudents = null, IEnumerable ratings = null) + { + Id = id; + Name = name; + Description = description; + StartDate = startDate; + EndDate = endDate; + Location = location; + Capacity = capacity; + Fee = fee; + Category = category; + State = state; + Organizer = organizer; + CoOrganizers = coOrganizers ?? Enumerable.Empty(); + InterestedStudents = interestedStudents ?? Enumerable.Empty(); + SignedUpStudents = signedUpStudents ?? Enumerable.Empty(); + Ratings = ratings ?? Enumerable.Empty(); + PublishDate = publishDate; + } + + public static Event Create(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, + Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, Organizer organizer) + { + var @event = new Event(id, name, description, startDate, endDate, location, capacity, fee, category, + state, publishDate, organizer); + return @event; + } + + public void AddOrganizer(Organizer organizer) + { + if (CoOrganizers.Any(o => o.Id == organizer.Id)) + { + throw new OrganizerAlreadyAddedException(organizer.Id); + } + + _coOrganizers.Add(organizer); + } + + public void SignUpStudent(Participant participant) + { + if (SignedUpStudents.Any(p => p.StudentId == participant.StudentId)) + { + throw new StudentAlreadySignedUpException(participant.StudentId, Id); + } + + if (SignedUpStudents.Count() >= Capacity) + { + throw new EventCapacityExceededException(Id, Capacity); + } + + _signedUpStudents.Add(participant); + } + + public void ShowStudentInterest(Participant participant) + { + if (InterestedStudents.Any(p => p.StudentId == participant.StudentId)) + { + throw new StudentAlreadyInterestedInEventException(participant.StudentId, Id); + } + + _interestedStudents.Add(participant); + } + + public void Rate(Guid studentId, int rating) + { + if(_signedUpStudents.All(p => p.StudentId != studentId)) + { + throw new StudentNotSignedUpForEventException(Id ,studentId); + } + + if (rating < 1 || rating > 5) + { + throw new InvalidRatingValueException(rating); + } + + if (_ratings.Any(r => r.StudentId == studentId)) + { + throw new StudentAlreadyRatedEventException(Id, studentId); + } + + _ratings.Add(new Rating(studentId, rating)); + } + + public bool UpdateState(DateTime now) + { + if(State == State.ToBePublished && PublishDate <= now) + { + ChangeState(State.Published); + } + else if (State == State.Published && EndDate <= now) + { + ChangeState(State.Archived); + } + else + return false; + + return true; + } + + private void ChangeState(State state) + { + if (State == state) + { + return; + } + + State = state; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Organizer.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Organizer.cs new file mode 100644 index 000000000..a2d988afa --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Organizer.cs @@ -0,0 +1,20 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Organizer + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Organization { get; set; } + + public Organizer(Guid id, string name, string email, string organization) + { + Id = id; + Name = name; + Email = email; + Organization = organization; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Participant.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Participant.cs new file mode 100644 index 000000000..e80fdda37 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Participant.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Participant(Guid studentId, string name, string email) + { + public Guid StudentId { get; set; } = studentId; + public string Name { get; set; } = name; + public string Email { get; set; } = email; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Rating.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Rating.cs new file mode 100644 index 000000000..faa1449d2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Rating.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Rating(Guid studentId, int value) + { + public Guid StudentId { get; set; } = studentId; + public int Value { get; set; } = value; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/State.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/State.cs new file mode 100644 index 000000000..42ac4f065 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/State.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Events.Core.Entities +{ + public enum State + { + ToBePublished, + Published, + Cancelled, + Archived + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Student.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Student.cs new file mode 100644 index 000000000..10b8469be --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Student.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Student(Guid id) + { + public Guid Id { get; set; } = id; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..7b18608d9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/DomainException.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/EventCapacityExceededException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/EventCapacityExceededException.cs new file mode 100644 index 000000000..f1de8727f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/EventCapacityExceededException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class EventCapacityExceededException : DomainException + { + public override string Code { get; } = "event_capacity_exceeded"; + public Guid Id { get; } + public int Capacity { get; } + public EventCapacityExceededException(Guid id, int capacity) : base($"Event with ID: {id} has exceeded its capacity." + + $" Maximum capacity is: {capacity}") + { + Id = id; + Capacity = capacity; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidAggregateId.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidAggregateId.cs new file mode 100644 index 000000000..0be5b0f3a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidAggregateId.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidRatingValueException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidRatingValueException.cs new file mode 100644 index 000000000..84d9ccb4e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidRatingValueException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class InvalidRatingValueException : DomainException + { + public override string Code { get; } = "invalid_rating_value"; + public int Rating { get; } + + public InvalidRatingValueException(int rating) : base($"Invalid rating value: {rating}. It must be between 1 and 5.") + { + Rating = rating; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerAlreadyAddedException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerAlreadyAddedException.cs new file mode 100644 index 000000000..13bed5f20 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerAlreadyAddedException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class OrganizerAlreadyAddedException : DomainException + { + public override string Code { get; } = "organizer_already_added"; + + public OrganizerAlreadyAddedException(Guid id) + : base($"Organizer with id: '{id}' has been already added") + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyInterestedInEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyInterestedInEventException.cs new file mode 100644 index 000000000..808ff80ec --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyInterestedInEventException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentAlreadyInterestedInEventException : DomainException + { + public override string Code { get; } = "student_already_interested_in_event"; + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentAlreadyInterestedInEventException(Guid studentId, Guid eventId) + : base($"Student with ID: {studentId} is already interested in event with ID: {eventId}.") + { + StudentId = studentId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs new file mode 100644 index 000000000..4d7821307 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadyRatedEventException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentAlreadyRatedEventException : DomainException + { + public override string Code { get; } = "student_already_rated_event"; + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentAlreadyRatedEventException(Guid eventId, Guid studentId) + : base($"Student with ID: '{studentId}' already rated event with ID: '{eventId}'.") + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadySignedUpException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadySignedUpException.cs new file mode 100644 index 000000000..f737d16fb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentAlreadySignedUpException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentAlreadySignedUpException : DomainException + { + public override string Code { get; } = "student_already_signed_up"; + public Guid StudentId { get; } + public Guid EventId { get; } + + public StudentAlreadySignedUpException(Guid studentId, Guid eventId) : base( + $"Student with ID: {studentId} already signed up to event with ID: {eventId}.") + { + StudentId = studentId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotSignedUpForEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotSignedUpForEventException.cs new file mode 100644 index 000000000..1103c572d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotSignedUpForEventException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentNotSignedUpForEventException : DomainException + { + public override string Code { get; } = "student_not_signed_for_event"; + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentNotSignedUpForEventException(Guid eventId, Guid studentId) + : base($"Student with ID: '{studentId}' is not signed for event with ID: '{eventId}'.") + { + EventId = eventId; + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/IDomainEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/IDomainEvent.cs new file mode 100644 index 000000000..a9105ed44 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/IDomainEvent.cs @@ -0,0 +1,6 @@ +namespace MiniSpace.Services.Events.Core +{ + public interface IDomainEvent + { + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.csproj new file mode 100644 index 000000000..3c20077e9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/MiniSpace.Services.Events.Core.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + disable + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs new file mode 100644 index 000000000..41d28e0f3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Application.Wrappers; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IEventRepository + { + Task GetAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Event @event); + Task UpdateAsync(Event @event); + Task DeleteAsync(Guid id); + Task,int,int,int,int>> BrowseAsync(int pageNumber, int pageSize, string name, + string organizer, DateTime dateFrom, DateTime dateTo, IEnumerable sortBy, string direction, + State state, IEnumerable eventIds = null); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..106ee0c72 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..f8d9b5ef6 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Events.Application.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.Events/src/MiniSpace.Services.Events.Core/Wrappers/Response.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/Response.cs new file mode 100644 index 000000000..4a9f844c1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/Response.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Events.Application.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.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..ef34a3812 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,28 @@ +using System; +using MiniSpace.Services.Events.Application; + +namespace MiniSpace.Services.Events.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(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..ac97d0a08 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Events.Application; + +namespace MiniSpace.Services.Events.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); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..1233da30b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.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; } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..2fae197c4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Events.Application; + +namespace MiniSpace.Services.Events.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("e-mail", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..7b69315d9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Events.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); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..88819cfd6 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + + +namespace MiniSpace.Services.Events.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); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..799ba2d49 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,22 @@ +using System; +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Events.Application.Events.Rejected; +using MiniSpace.Services.Events.Application.Exceptions; + +namespace MiniSpace.Services.Events.Infrastructure.Exceptions +{ + public class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + // TODO: Add more exceptions + AuthorizedUserIsNotAnOrganizerException ex => new AddEventRejected(ex.UserId, ex.Message, ex.Code), + OrganizerCannotAddEventForAnotherOrganizerException ex => new AddEventRejected(ex.OrganizerId, ex.Message, ex.Code), + InvalidEventCategoryException ex => new AddEventRejected(Guid.Empty, ex.Message, ex.Code), + InvalidEventDateTimeException ex => new AddEventRejected(Guid.Empty, ex.Message, ex.Code), + InvalidEventDateTimeOrderException ex => new AddEventRejected(Guid.Empty, ex.Message, ex.Code), + _ => null, + }; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..84acc66f1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Core.Exceptions; + +namespace MiniSpace.Services.Events.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; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs new file mode 100644 index 000000000..82a642ba3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +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.Swagger; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; +using MiniSpace.Services.Events.Application.Services.Events; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Contexts; +using MiniSpace.Services.Events.Infrastructure.Decorators; +using MiniSpace.Services.Events.Infrastructure.Exceptions; +using MiniSpace.Services.Events.Infrastructure.Logging; +using MiniSpace.Services.Events.Infrastructure.Mongo; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Events.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Events.Infrastructure.Services; +using MiniSpace.Services.Events.Infrastructure.Services.Clients; +using MiniSpace.Services.Events.Infrastructure.Services.Workers; + +namespace MiniSpace.Services.Events.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + 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<>)); + builder.Services.AddHostedService(); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddExceptionToMessageMapper() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("events") + .AddMongoRepository("students") + .AddWebApiSwaggerDocs() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + //.UseMongo() + .UsePublicContracts() + .UseMetrics() + .UseAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .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; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..0250466c9 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Events.Application; + +namespace MiniSpace.Services.Events.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..649edaf0e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Events.Application.Commands; + +namespace MiniSpace.Services.Events.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(DeleteEvent).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..66a7e31af --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using Convey.Logging.CQRS; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Application.Exceptions; + +namespace MiniSpace.Services.Events.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(AddEvent), + new HandlerLogTemplate + { + After = "Added an event with id: {EventId}." + } + }, + { + typeof(DeleteEvent), + new HandlerLogTemplate + { + After = "Event with id: {EventId} has been deleted." + } + }, + { + typeof(RateEvent), + new HandlerLogTemplate + { + After = "Rated an event with id: {EventId} with rating: {Rating}." + } + }, + { + typeof(SearchEvents), + new HandlerLogTemplate + { + After = "Searching events with name: {Name}, organizer: {Organizer}." + } + }, + { + typeof(ShowInterestInEvent), + new HandlerLogTemplate + { + After = "Student with id: {StudentId} showed interest in event with id: {EventId}." + } + }, + { + typeof(SignUpToEvent), + new HandlerLogTemplate + { + After = "Student with id: {StudentId} signed up to event with id: {EventId}." + } + }, + { + typeof(UpdateEventsState), + new HandlerLogTemplate + { + After = "Events' state updated at: {Now}." + } + }, + { + typeof(StudentCreated), + new HandlerLogTemplate + { + After = "Added a student with id: {StudentId}", + OnError = new Dictionary + { + { + typeof(StudentAlreadyAddedException), + "Student with id: {StudentId} was already added." + + } + } + } + }, + }; + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var key = message.GetType(); + return MessageTemplates.TryGetValue(key, out var template) ? template : null; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj new file mode 100644 index 000000000..af01e2591 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs new file mode 100644 index 000000000..9c8c88738 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Convey.Types; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class EventDocument : IIdentifiable + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public Organizer Organizer { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public IEnumerable CoOrganizers { get; set; } + public Address Location { get; set; } + //public string Image { get; set; } + public IEnumerable InterestedStudents { get; set; } + public IEnumerable SignedUpStudents { get; set; } + public int Capacity { get; set; } + public decimal Fee { get; set; } + public Category Category { get; set; } + public State State { get; set; } + public DateTime PublishDate { get; set; } + public IEnumerable Ratings { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..271978669 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,87 @@ +using System.Linq; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static EventDto AsDto(this EventDocument document) + => new () + { + Id = document.Id, + Name = document.Name, + Description = document.Description, + Organizer = document.Organizer.AsDto(), + StartDate = document.StartDate, + EndDate = document.EndDate, + CoOrganizers = document.CoOrganizers.Select(x => x.AsDto()), + Location = document.Location.AsDto(), + InterestedStudents = document.InterestedStudents.Count(), + SignedUpStudents = document.SignedUpStudents.Count(), + Capacity = document.Capacity, + Fee = document.Fee, + Category = document.Category.ToString(), + Status = document.State.ToString(), + PublishDate = document.PublishDate + }; + + public static Event AsEntity(this EventDocument document) + => new (document.Id, document.Name, document.Description, document.StartDate, document.EndDate, + document.Location, document.Capacity, document.Fee, document.Category, document.State, document.PublishDate, + document.Organizer, document.CoOrganizers, document.InterestedStudents, document.SignedUpStudents, document.Ratings); + + public static EventDocument AsDocument(this Event entity) + => new () + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + Organizer = entity.Organizer, + StartDate = entity.StartDate, + EndDate = entity.EndDate, + Location = entity.Location, + CoOrganizers = entity.CoOrganizers, + InterestedStudents = entity.InterestedStudents, + SignedUpStudents = entity.SignedUpStudents, + Capacity = entity.Capacity, + Fee = entity.Fee, + Category = entity.Category, + State = entity.State, + PublishDate = entity.PublishDate, + Ratings = entity.Ratings + }; + + public static AddressDto AsDto(this Address entity) + => new () + { + BuildingName = entity.BuildingName, + Street = entity.Street, + BuildingNumber = entity.BuildingNumber, + ApartmentNumber = entity.ApartmentNumber, + City = entity.City, + ZipCode = entity.ZipCode + }; + + public static Address AsEntity(this AddressDto dto) + => new (dto.BuildingName, dto.Street, dto.BuildingNumber, dto.ApartmentNumber, dto.City, dto.ZipCode); + + public static OrganizerDto AsDto(this Organizer entity) + => new () + { + Id = entity.Id, + Name = entity.Name, + Email = entity.Email, + Organization = entity.Organization + }; + + 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.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..033e14777 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,10 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class StudentDocument: IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs new file mode 100644 index 000000000..e840928e4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers +{ + public class GetEventHandler : IQueryHandler + { + private readonly IMongoRepository _eventRepository; + private readonly IMessageBroker _messageBroker; + + public GetEventHandler(IMongoRepository eventRepository, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(GetEvent query, CancellationToken cancellationToken) + { + var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); + + await _messageBroker.PublishAsync(new EventViewed(query.EventId)); + return document?.AsDto(); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs new file mode 100644 index 000000000..424ef7dc7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; +using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentEventsHandler : IQueryHandler>> + { + private readonly IEventRepository _eventRepository; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly IAppContext _appContext; + + public GetStudentEventsHandler(IEventRepository eventRepository, + IStudentsServiceClient studentsServiceClient, IAppContext appContext) + { + _eventRepository = eventRepository; + _studentsServiceClient = studentsServiceClient; + _appContext = appContext; + } + + public async Task>> HandleAsync(GetStudentEvents query, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != query.StudentId) + { + return new PagedResponse>(Enumerable.Empty(), + 1, query.numberOfResults, 0, 0); + } + + var studentEvents = await _studentsServiceClient.GetAsync(query.StudentId); + var studentEventIds = studentEvents.InterestedInEvents.Union(studentEvents.SignedUpEvents); + + var result = await _eventRepository.BrowseAsync(1, query.numberOfResults, + string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue, + Enumerable.Empty(), "asc", State.Published, studentEventIds); + + return new PagedResponse>(result.Item1.Select(e => new EventDto(e)), + result.Item2, result.Item3, result.Item4, result.Item5);; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs new file mode 100644 index 000000000..2fda11c8e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Events.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(o => o.Id == id); + + return @event?.AsEntity(); + } + + public async Task> GetAllAsync() + { + var events = _repository.Collection.AsQueryable(); + var filteredEvents = await events.Where(e + => e.State == State.ToBePublished || e.State == State.Published).ToListAsync(); + return filteredEvents.Select(e => e.AsEntity()); + } + + public async Task,int,int,int,int>> BrowseAsync(int pageNumber, int pageSize, string name, string organizer, + DateTime dateFrom, DateTime dateTo, IEnumerable sortBy, string direction, State state, + IEnumerable eventIds = null) + { + var filterDefinition = Repositories.Extensions.ToFilterDefinition(name, organizer, dateFrom, dateTo, state, eventIds); + var sortDefinition = Repositories.Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await _repository.Collection.AggregateByPage( + filterDefinition, + sortDefinition, + pageNumber, + pageSize); + + return new Tuple,int,int,int,int>(pagedEvents.data.Select(e => e.AsEntity()), + pageNumber, pageSize, pagedEvents.totalPages, pagedEvents.totalElements); + } + + public Task AddAsync(Event @event) => _repository.AddAsync(@event.AsDocument()); + public Task UpdateAsync(Event @event) => _repository.UpdateAsync(@event.AsDocument()); + public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs new file mode 100644 index 000000000..17ec0605f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/Extensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public static class Extensions + { + 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; + + 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(string name, string organizer, + DateTime dateFrom, DateTime dateTo, State state, IEnumerable eventIds = null) + { + var filterDefinitionBuilder = Builders.Filter; + var filterDefinition = filterDefinitionBuilder.Empty; + filterDefinition &= filterDefinitionBuilder.Eq(x => x.State, state); + + if (!string.IsNullOrWhiteSpace(name)) + { + filterDefinition &= filterDefinitionBuilder.Regex(x => x.Name, + new BsonRegularExpression(new Regex($".*{name}.*", RegexOptions.IgnoreCase))); + } + + if (!string.IsNullOrWhiteSpace(organizer)) + { + filterDefinition &= filterDefinitionBuilder.Regex(x => x.Organizer.Name, + new BsonRegularExpression(new Regex($".*{organizer}.*", RegexOptions.IgnoreCase))); + } + + if (dateFrom != DateTime.MinValue) + { + filterDefinition &= filterDefinitionBuilder.Gte(x => x.StartDate, dateFrom); + } + + if (dateTo != DateTime.MinValue) + { + filterDefinition &= filterDefinitionBuilder.Lte(x => x.EndDate, dateTo); + } + + if (eventIds != null) + { + filterDefinition &= filterDefinitionBuilder.In(x => x.Id, eventIds); + } + + return filterDefinition; + } + + public static SortDefinition ToSortDefinition(IEnumerable sortByArguments, string direction) + { + var sort = sortByArguments.ToList(); + if(!sort.Any()) + { + sort.Add("PublishDate"); + } + 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.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 000000000..bff1d7339 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.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.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 000000000..9bde50992 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Services.Clients; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Clients +{ + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}/events"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..43c28aa00 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,10 @@ +using System; +using MiniSpace.Services.Events.Application.Services; + +namespace MiniSpace.Services.Events.Infrastructure.Services +{ + public class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..38fe02a95 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core; + +namespace MiniSpace.Services.Events.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + // TODO: update mapper + // switch (@event) + // { + // + // } + + return null; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs new file mode 100644 index 000000000..66f36cd55 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs @@ -0,0 +1,48 @@ +using System; +using System.Globalization; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Services +{ + public class EventValidator: IEventValidator + { + private readonly string _expectedFormat = "yyyy-MM-ddTHH:mm:ss.fffZ"; + + public Category ParseCategory(string categoryString) + { + if (!Enum.TryParse(categoryString, true, out var category)) + { + throw new InvalidEventCategoryException(categoryString); + } + return category; + } + + public DateTime ParseDate(string dateString, string fieldName) + { + if (!DateTime.TryParseExact(dateString, _expectedFormat, CultureInfo.InvariantCulture, + DateTimeStyles.None, out DateTime date)) + { + throw new InvalidEventDateTimeException(fieldName, dateString); + } + return date; + } + + public void ValidateDates(DateTime earlierDate, DateTime laterDate, string earlierDateField, string laterDateField) + { + if (laterDate <= earlierDate) + throw new InvalidEventDateTimeOrderException(earlierDate, laterDate, + earlierDateField, laterDateField); + } + + public (int pageNumber, int pageSize) PageFilter(int pageNumber, int pageSize) + { + pageNumber = pageNumber < 0 ? 0 : pageNumber; + pageSize = pageSize > 10 ? 10 : pageSize; + return (pageNumber, pageSize); + } + + // TODO: Add Address validation + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..a72c57f31 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +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.Events.Application.Services; + +namespace MiniSpace.Services.Events.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); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Workers/EventStateUpdaterWorker.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Workers/EventStateUpdaterWorker.cs new file mode 100644 index 000000000..0083c2815 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Workers/EventStateUpdaterWorker.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using Convey.Persistence.MongoDB; +using Microsoft.Extensions.Hosting; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.Events; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Workers +{ + public class EventStateUpdaterWorker: BackgroundService + { + private readonly IMessageBroker _messageBroker; + private readonly ICommandDispatcher _commandDispatcher; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(10); + + public EventStateUpdaterWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, + IDateTimeProvider dateTimeProvider) + { + _messageBroker = messageBroker; + _commandDispatcher = commandDispatcher; + _dateTimeProvider = dateTimeProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _messageBroker.PublishAsync(new EventBackgroundWorkerStarted("state_updater")); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await _commandDispatcher.SendAsync(new UpdateEventsState(_dateTimeProvider.Now), stoppingToken); + await Task.Delay(_updateInterval, stoppingToken); + } + catch (TaskCanceledException) + { + await _messageBroker.PublishAsync(new EventBackgroundWorkerStopped("state_updater")); + return; + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/Dockerfile b/MiniSpace.Services.Identity/Dockerfile index fb52642bd..2696be2f4 100644 --- a/MiniSpace.Services.Identity/Dockerfile +++ b/MiniSpace.Services.Identity/Dockerfile @@ -1,11 +1,17 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app + COPY . . -RUN dotnet publish src/MiniSpace.Services.Identity.Api -c release -o out -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 +RUN dotnet publish src/MiniSpace.Services.Identity.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app + COPY --from=build /app/out . -ENV ASPNETCORE_URLS http://*:80 -ENV ASPNETCORE_ENVIRONMENT docker -ENTRYPOINT dotnet MiniSpace.Services.Identity.Api.dll \ No newline at end of file + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Identity.Api.dll"] diff --git a/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh index 054b79c54..5423a1c01 100755 --- a/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh +++ b/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh @@ -1,6 +1,6 @@ #!/bin/bash -export ASPNETCORE_ENVIRONMENT=Development +export ASPNETCORE_ENVIRONMENT=docker cd .. diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.csproj b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.csproj index 2d4086edf..3c7aac04b 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.csproj +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 latest MiniSpace.Services.Identity.Api diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.sln b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.sln new file mode 100644 index 000000000..dfe5b79be --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/MiniSpace.Services.Identity.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Identity.Api", "MiniSpace.Services.Identity.Api.csproj", "{9A667DA7-F3BE-412E-919E-CF99256B8736}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9A667DA7-F3BE-412E-919E-CF99256B8736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A667DA7-F3BE-412E-919E-CF99256B8736}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A667DA7-F3BE-412E-919E-CF99256B8736}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A667DA7-F3BE-412E-919E-CF99256B8736}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0AF810AB-F211-4F82-9813-5CFA54689485} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs index 741954c29..4b80854a3 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs @@ -69,9 +69,28 @@ public static async Task Main(string[] args) await ctx.RequestServices.GetService().RevokeAsync(cmd.RefreshToken); ctx.Response.StatusCode = 204; }) + .Post("users/{userId}/organizer-rights", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().GrantOrganizerRightsAsync(cmd); + ctx.Response.StatusCode = 204; + }) + .Delete("users/{userId}/organizer-rights", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().RevokeOrganizerRightsAsync(cmd); + ctx.Response.StatusCode = 204; + }) + .Post("users/{userId}/ban", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().BanUserAsync(cmd); + ctx.Response.StatusCode = 204; + }) + .Delete("users/{userId}/ban", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().UnbanUserAsync(cmd); + ctx.Response.StatusCode = 204; + }) )) .UseLogging() - .UseVault() .Build() .RunAsync(); diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.docker.json b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.docker.json index eaf74f99c..ab125a5eb 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.docker.json +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.docker.json @@ -32,7 +32,7 @@ }, "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", "expiryMinutes": 60, - "issuer": "pacco", + "issuer": "minispace", "validateAudience": false, "validateIssuer": false, "validateLifetime": true, @@ -71,12 +71,12 @@ "influxEnabled": false, "prometheusEnabled": true, "influxUrl": "http://influx:8086", - "database": "pacco", + "database": "minispace", "env": "docker", "interval": 5 }, "mongo": { - "connectionString": "mongodb://mongo:27017", + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", "database": "identity-service", "seed": false }, diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json index 6835aef77..11020114e 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json @@ -30,11 +30,6 @@ } }, "jwt": { - "certificate": { - "location": "certs/localhost.pfx", - "password": "test", - "rawData": "" - }, "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", "expiryMinutes": 60, "issuer": "minispace", @@ -154,7 +149,7 @@ "spanContextHeader": "span_context" }, "redis": { - "connectionString": "localhost", + "connectionString": "redis", "instance": "identity:" }, "swagger": { diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.local.json b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.local.json index 6981002c1..aed2d36c8 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.local.json +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.local.json @@ -1,55 +1,194 @@ { + "app": { + "name": "MiniSpace Identity Service", + "service": "identity-service", + "version": "1" + }, "consul": { - "enabled": false, - "pingEndpoint": "" + "enabled": true, + "url": "http://localhost:8500", + "service": "identity-service", + "address": "docker.for.win.localhost", + "port": "5004", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 }, "fabio": { - "enabled": false + "enabled": true, + "url": "http://localhost:9999", + "service": "identity-service" }, "httpClient": { - "type": "", + "type": "fabio", "retries": 3, - "services": {} + "services": {}, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": false, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] }, "logger": { - "level": "verbose", + "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": false + "enabled": true, + "path": "logs/logs.txt", + "interval": "day" }, "seq": { - "enabled": false - } + "enabled": true, + "url": "http://localhost:5341", + "apiKey": "secret" + }, + "tags": {} }, "jaeger": { - "enabled": false - }, - "jwt": { - "certificate": { - "location": "", - "password": "", - "rawData": "" - }, - "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij" + "enabled": true, + "serviceName": "identity", + "udpHost": "localhost", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] }, "metrics": { - "enabled": false, - "prometheusEnabled": false + "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": "identity-service", + "seed": false }, "outbox": { - "enabled": false + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "identity-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": "identity" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "identity-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "identity:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true }, "vault": { - "enabled": false, + "enabled": true, + "url": "http://localhost:8200", + "authType": "token", + "token": "secret", + "username": "user", + "password": "secret", "kv": { - "enabled": false + "enabled": true, + "engineVersion": 2, + "mountPoint": "kv", + "path": "identity-service/settings" }, "pki": { - "enabled": false + "enabled": true, + "roleName": "identity-service", + "commonName": "identity-service.pacco.io" }, "lease": { "mongo": { - "enabled": false + "type": "database", + "roleName": "identity-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } } } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/BanUser.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/BanUser.cs new file mode 100644 index 000000000..727bce93c --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/BanUser.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class BanUser(Guid userId) : ICommand + { + public Guid UserId { get; } = userId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs new file mode 100644 index 000000000..a6c4a04dc --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class GrantOrganizerRights(Guid userId) : ICommand + { + public Guid UserId { get; } = userId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs new file mode 100644 index 000000000..c43cd6305 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs @@ -0,0 +1,15 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class RevokeOrganizerRights : ICommand + { + public Guid UserId { get; } + + public RevokeOrganizerRights(Guid userId) + { + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignUp.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignUp.cs index 3139fb14c..71b09a027 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignUp.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignUp.cs @@ -8,14 +8,19 @@ namespace MiniSpace.Services.Identity.Application.Commands public class SignUp : ICommand { public Guid UserId { get; } + public string FirstName { get; } + public string LastName { get; } public string Email { get; } public string Password { get; } public string Role { get; } public IEnumerable Permissions { get; } - public SignUp(Guid userId, string email, string password, string role, IEnumerable permissions) + public SignUp(Guid userId, string firstName, string lastName, string email, string password, string role, + IEnumerable permissions) { UserId = userId == Guid.Empty ? Guid.NewGuid() : userId; + FirstName = firstName; + LastName = lastName; Email = email; Password = password; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UnbanUser.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UnbanUser.cs new file mode 100644 index 000000000..4351465bb --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UnbanUser.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class UnbanUser(Guid userId) : ICommand + { + public Guid UserId { get; } = userId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs index 752512927..af711203e 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs @@ -7,6 +7,7 @@ namespace MiniSpace.Services.Identity.Application.DTO public class UserDto { public Guid Id { get; set; } + public string Name { get; set; } public string Email { get; set; } public string Role { get; set; } public DateTime CreatedAt { get; set; } @@ -19,6 +20,7 @@ public UserDto() public UserDto(User user) { Id = user.Id; + Name = user.Name; Email = user.Email; Role = user.Role; CreatedAt = user.CreatedAt; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsGranted.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsGranted.cs new file mode 100644 index 000000000..63a1a8206 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsGranted.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Identity.Application.Events +{ + public class OrganizerRightsGranted : IEvent + { + public Guid UserId { get; } + public OrganizerRightsGranted(Guid userId) + { + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsRevoked.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsRevoked.cs new file mode 100644 index 000000000..9bea2bbe1 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/OrganizerRightsRevoked.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Identity.Application.Events +{ + public class OrganizerRightsRevoked : IEvent + { + public Guid UserId { get; } + public OrganizerRightsRevoked(Guid userId) + { + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs index 2c168f8f9..14053ced5 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs @@ -7,12 +7,16 @@ namespace MiniSpace.Services.Identity.Application.Events public class SignedUp : IEvent { public Guid UserId { get; } + public string FirstName { get; } + public string LastName { get; } public string Email { get; } public string Role { get; } - public SignedUp(Guid userId, string email, string role) + public SignedUp(Guid userId, string firstName, string lastName, string email, string role) { UserId = userId; + FirstName = firstName; + LastName = lastName; Email = email; Role = role; } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserBanned.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserBanned.cs new file mode 100644 index 000000000..031d5dd81 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserBanned.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Identity.Application.Events +{ + public class UserBanned(Guid userId) : IEvent + { + public Guid UserId { get; } = userId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserUnbanned.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserUnbanned.cs new file mode 100644 index 000000000..294f421a1 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/UserUnbanned.cs @@ -0,0 +1,10 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Identity.Application.Events +{ + public class UserUnbanned(Guid userId) : IEvent + { + public Guid UserId { get; } = userId; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs index 365e90998..d5f13474d 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs @@ -7,8 +7,12 @@ 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; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/MiniSpace.Services.Identity.Application.csproj b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/MiniSpace.Services.Identity.Application.csproj index 78085536e..f42419f97 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/MiniSpace.Services.Identity.Application.csproj +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/MiniSpace.Services.Identity.Application.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs index e6b8f05be..bf8dd7d40 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs @@ -10,5 +10,9 @@ public interface IIdentityService Task GetAsync(Guid id); Task SignInAsync(SignIn command); Task SignUpAsync(SignUp command); + Task GrantOrganizerRightsAsync(GrantOrganizerRights command); + Task RevokeOrganizerRightsAsync(RevokeOrganizerRights command); + Task BanUserAsync(BanUser command); + Task UnbanUserAsync(UnbanUser command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs index 128c02989..c56bd3156 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs @@ -7,6 +7,7 @@ using MiniSpace.Services.Identity.Application.Commands; using MiniSpace.Services.Identity.Application.DTO; using MiniSpace.Services.Identity.Application.Events; +using MiniSpace.Services.Identity.Application.Exceptions; using MiniSpace.Services.Identity.Core.Entities; using MiniSpace.Services.Identity.Core.Exceptions; using MiniSpace.Services.Identity.Core.Repositories; @@ -70,7 +71,9 @@ public async Task SignInAsync(SignIn command) var claims = user.Permissions.Any() ? new Dictionary> { - ["permissions"] = user.Permissions + ["permissions"] = user.Permissions, + ["name"] = new [] { user.Name }, + ["e-mail"] = new [] { user.Email } } : null; var auth = _jwtProvider.Create(user.Id, user.Role, claims: claims); @@ -99,11 +102,77 @@ public async Task SignUpAsync(SignUp command) var role = string.IsNullOrWhiteSpace(command.Role) ? "user" : command.Role.ToLowerInvariant(); var password = _passwordService.Hash(command.Password); - user = new User(command.UserId, command.Email, password, role, DateTime.UtcNow, command.Permissions); + user = new User(command.UserId, $"{command.FirstName} {command.LastName}", command.Email, password, + role, DateTime.UtcNow, command.Permissions); await _userRepository.AddAsync(user); _logger.LogInformation($"Created an account for the user with id: {user.Id}."); - await _messageBroker.PublishAsync(new SignedUp(user.Id, user.Email, user.Role)); + await _messageBroker.PublishAsync(new SignedUp(user.Id, command.FirstName, command.LastName, + user.Email, user.Role)); + } + + public async Task GrantOrganizerRightsAsync(GrantOrganizerRights command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user is null) + { + _logger.LogError($"User with id: {command.UserId} was not found."); + throw new UserNotFoundException(command.UserId); + } + + user.GrantOrganizerRights(); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Granted organizer rights to the user with id: {user.Id}."); + await _messageBroker.PublishAsync(new OrganizerRightsGranted(user.Id)); + } + + public async Task RevokeOrganizerRightsAsync(RevokeOrganizerRights command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user is null) + { + _logger.LogError($"User with id: {command.UserId} was not found."); + throw new UserNotFoundException(command.UserId); + } + + user.RevokeOrganizerRights(); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Revoked organizer rights from the user with id: {user.Id}."); + await _messageBroker.PublishAsync(new OrganizerRightsRevoked(user.Id)); + } + + public async Task BanUserAsync(BanUser command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user is null) + { + _logger.LogError($"User with id: {command.UserId} was not found."); + throw new UserNotFoundException(command.UserId); + } + + user.Ban(); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Banned the user with id: {user.Id}."); + await _messageBroker.PublishAsync(new UserBanned(user.Id)); + } + + public async Task UnbanUserAsync(UnbanUser command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user is null) + { + _logger.LogError($"User with id: {command.UserId} was not found."); + throw new UserNotFoundException(command.UserId); + } + + user.Unban(); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Unbanned the user with id: {user.Id}."); + await _messageBroker.PublishAsync(new UserUnbanned(user.Id)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs index a56fbc9b1..6373f3673 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs @@ -4,6 +4,8 @@ public static class Role { public const string User = "user"; public const string Admin = "admin"; + public const string Banned = "banned"; + public const string Organizer = "organizer"; public static bool IsValid(string role) { @@ -14,7 +16,7 @@ public static bool IsValid(string role) role = role.ToLowerInvariant(); - return role == User || role == Admin; + return role == User || role == Admin || role == Banned || role == Organizer; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs index ce640b52e..5aabedc54 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs @@ -7,15 +7,21 @@ namespace MiniSpace.Services.Identity.Core.Entities { public class User : AggregateRoot { + public string Name { get; private set; } public string Email { get; private set; } public string Role { get; private set; } public string Password { get; private set; } public DateTime CreatedAt { get; private set; } public IEnumerable Permissions { get; private set; } - public User(Guid id, string email, string password, string role, DateTime createdAt, + public User(Guid id, string name, string email, string password, string role, DateTime createdAt, IEnumerable permissions = null) { + if(string.IsNullOrWhiteSpace(name)) + { + throw new InvalidNameException(name); + } + if (string.IsNullOrWhiteSpace(email)) { throw new InvalidEmailException(email); @@ -32,11 +38,57 @@ public User(Guid id, string email, string password, string role, DateTime create } Id = id; + Name = name; Email = email.ToLowerInvariant(); Password = password; Role = role.ToLowerInvariant(); CreatedAt = createdAt; Permissions = permissions ?? Enumerable.Empty(); } + + public void GrantOrganizerRights() + { + if (Role != Entities.Role.User) + { + throw new UserCannotBecomeAnOrganizerException(Id, Role); + } + + Role = Entities.Role.Organizer; + } + + public void RevokeOrganizerRights() + { + if (Role != Entities.Role.Organizer) + { + throw new UserIsNotAnOrganizerException(Id); + } + + Role = Entities.Role.User; + } + + public void Ban() + { + if (Role == Entities.Role.Banned || Role == Entities.Role.Admin) + { + throw new UserCannotBeBannedException(Id, Role); + } + + Role = Entities.Role.Banned; + } + + public void Unban() + { + if (Role != Entities.Role.Banned) + { + throw new UserIsNotBannedException(Id, Role); + } + + Role = Entities.Role.User; + } + } + + public static class UserPermissions + { + public static string OrganizeEvents { get; private set; } = "organize_events"; } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/InvalidNameException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/InvalidNameException.cs new file mode 100644 index 000000000..aa43f0ad4 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/InvalidNameException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Identity.Core.Exceptions +{ + public class InvalidNameException : DomainException + { + public override string Code { get; } = "invalid_name"; + + public InvalidNameException(string name) : base($"Invalid name: {name}.") + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs new file mode 100644 index 000000000..1aabc2fb9 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Identity.Core.Exceptions +{ + public class UserCannotBeBannedException : DomainException + { + public override string Code { get; } = "user_cannot_be_banned"; + public Guid UserId { get; } + public string Role { get; } + + public UserCannotBeBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} cannot be banned.") + { + UserId = userId; + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs new file mode 100644 index 000000000..b69237795 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Identity.Core.Exceptions +{ + public class UserCannotBecomeAnOrganizerException : DomainException + { + public override string Code { get; } = "user_cannot_become_an_organizer"; + public Guid UserId { get; } + public string Role { get; } + + public UserCannotBecomeAnOrganizerException(Guid userId, string role) : base($"User with ID: {userId} and role: {role} is cannot become an organizer.") + { + UserId = userId; + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs new file mode 100644 index 000000000..c4987758c --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Identity.Core.Exceptions +{ + public class UserIsNotAnOrganizerException : DomainException + { + public override string Code { get; } = "user_is_not_an_organizer"; + public Guid UserId { get; } + + public UserIsNotAnOrganizerException(Guid userId) : base($"User with ID: {userId} is not an organizer.") + { + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs new file mode 100644 index 000000000..34cfe793c --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Identity.Core.Exceptions +{ + public class UserIsNotBannedException : DomainException + { + public override string Code { get; } = "user_is_not_banned"; + public Guid UserId { get; } + public string Role { get; } + + public UserIsNotBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} is not banned.") + { + UserId = userId; + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/MiniSpace.Services.Identity.Core.csproj b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/MiniSpace.Services.Identity.Core.csproj index 540a3f68f..d36f76db0 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/MiniSpace.Services.Identity.Core.csproj +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/MiniSpace.Services.Identity.Core.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Repositories/IUserRepository.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Repositories/IUserRepository.cs index 1c0df246a..939dd828a 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Repositories/IUserRepository.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Repositories/IUserRepository.cs @@ -9,5 +9,6 @@ public interface IUserRepository Task GetAsync(Guid id); Task GetAsync(string email); Task AddAsync(User user); + Task UpdateAsync(User user); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs index f648b0bcb..e20bc38f9 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs @@ -8,8 +8,12 @@ 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() @@ -27,7 +31,11 @@ internal IdentityContext(string id, string role, bool isAuthenticated, IDictiona 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.Identity/src/MiniSpace.Services.Identity.Infrastructure/MiniSpace.Services.Identity.Infrastructure.csproj b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/MiniSpace.Services.Identity.Infrastructure.csproj index 585cc42fd..033db66d7 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/MiniSpace.Services.Identity.Infrastructure.csproj +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/MiniSpace.Services.Identity.Infrastructure.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net8.0 diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs index 67c09612b..ed27c6059 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs @@ -7,13 +7,14 @@ namespace MiniSpace.Services.Identity.Infrastructure.Mongo.Documents internal static class Extensions { public static User AsEntity(this UserDocument document) - => new User(document.Id, document.Email, document.Password, document.Role, document.CreatedAt, + => new User(document.Id, document.Name, document.Email, document.Password, document.Role, document.CreatedAt, document.Permissions); public static UserDocument AsDocument(this User entity) => new UserDocument { Id = entity.Id, + Name = entity.Name, Email = entity.Email, Password = entity.Password, Role = entity.Role, @@ -25,6 +26,7 @@ public static UserDto AsDto(this UserDocument document) => new UserDto { Id = document.Id, + Name = document.Name, Email = document.Email, Role = document.Role, CreatedAt = document.CreatedAt, diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs index e26c3c3b7..1678b8d13 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs @@ -7,6 +7,7 @@ namespace MiniSpace.Services.Identity.Infrastructure.Mongo.Documents internal sealed class UserDocument : IIdentifiable { public Guid Id { get; set; } + public string Name { get; set; } public string Email { get; set; } public string Role { get; set; } public string Password { get; set; } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Repositories/UserRepository.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Repositories/UserRepository.cs index 6fc8f5615..7f1b04f1a 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Repositories/UserRepository.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Repositories/UserRepository.cs @@ -31,5 +31,6 @@ public async Task GetAsync(string email) } public Task AddAsync(User user) => _repository.AddAsync(user.AsDocument()); + public Task UpdateAsync(User user) => _repository.UpdateAsync(user.AsDocument()); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Commands/Handlers/SignUpHandlerTests.cs b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Commands/Handlers/SignUpHandlerTests.cs index 04261350e..6bbb236f9 100644 --- a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Commands/Handlers/SignUpHandlerTests.cs +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Commands/Handlers/SignUpHandlerTests.cs @@ -23,7 +23,7 @@ public SignUpHandlerTests() public async Task HandleAsync_CallsSignUpAsync() { // Arrange - var command = new SignUp(Guid.NewGuid(), "test@email.com", "password", "user", new List()); + var command = new SignUp(Guid.NewGuid(), "firstName", "lastName","test@email.com", "password", "user", new List()); // Act await _signUpHandler.HandleAsync(command); diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/IdentityServiceTests.cs b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/IdentityServiceTests.cs index e1e649e65..ee802f51a 100644 --- a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/IdentityServiceTests.cs +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/IdentityServiceTests.cs @@ -50,7 +50,7 @@ public async Task GetAsync_WithValidId_ReturnsUserDto() { //Arrange var userId = Guid.NewGuid(); - var user = new User(userId, "test@gmail.com", "password", "user", DateTime.UtcNow, new List()); + var user = new User(userId, "name", "test@gmail.com", "password", "user", DateTime.UtcNow, new List()); _mockUserRepository.Setup(x => x.GetAsync(userId)).ReturnsAsync(user); //Act @@ -82,7 +82,7 @@ public async Task SignInAsync_WithValidCredentials_ReturnsAuthDto() //Arrange var command = new SignIn("validEmail@gmail.com", "password"); var userId = Guid.NewGuid(); - var user = new User(userId, command.Email, command.Password, "user", DateTime.UtcNow, new List()); + var user = new User(userId, "name", command.Email, command.Password, "user", DateTime.UtcNow, new List()); var authDto = new AuthDto(); _mockUserRepository.Setup(x => x.GetAsync(command.Email)).ReturnsAsync(user); _mockPasswordService.Setup(x => x.IsValid(user.Password, command.Password)).Returns(true); @@ -130,7 +130,7 @@ public async Task SignInAsync_WithInvalidPassword_ThrowsInvalidCredentialsExcept //Arrange var command = new SignIn("validEmail@gmail.com", "invalidPassword"); var userId = Guid.NewGuid(); - var user = new User(userId, command.Email, command.Password, "user", DateTime.UtcNow, new List()); + var user = new User(userId, "name", command.Email, command.Password, "user", DateTime.UtcNow, new List()); _mockUserRepository.Setup(x => x.GetAsync(command.Email)).ReturnsAsync(user); _mockPasswordService.Setup(x => x.IsValid(user.Password, command.Password)).Returns(false); @@ -145,6 +145,8 @@ public async Task SignUpAsync_WithValidCredentials_CreatesUser() //Arrange var command = new SignUp( Guid.NewGuid(), + "fitstName", + "lastName", "validEmail@gmail.com", "password", "", @@ -166,7 +168,7 @@ public async Task SignUpAsync_WithValidCredentials_CreatesUser() user.Password.Should().NotBe(command.Password); user.Password.Should().Be("hashedPassword"); signedUp.Should().NotBeNull(); - signedUp.Should().BeEquivalentTo(new SignedUp(user.Id, user.Email, user.Role)); + signedUp.Should().BeEquivalentTo(new SignedUp(user.Id, command.FirstName, command.LastName, user.Email, user.Role)); _mockUserRepository.Verify(x => x.AddAsync(It.IsAny()), Times.Once); _mockMessageBroker.Verify(x => x.PublishAsync(It.IsAny()), Times.Once); } @@ -177,6 +179,8 @@ public async Task SignUpAsync_WithInvalidEmail_ThrowsInvalidEmailException() //Arrange var command = new SignUp( Guid.NewGuid(), + "firstName", + "lastName", "invalidEmail", "password", "user", @@ -193,11 +197,13 @@ public async Task SignUpAsync_WhenEmailInUse_ThrowsEmailInUseException() //Arrange var command = new SignUp( Guid.NewGuid(), + "firstName", + "lastName", "emailInUse@gmail.com", "password", "user", new List()); - var user = new User(Guid.NewGuid(), command.Email, command.Password, "user", DateTime.UtcNow, new List()); + var user = new User(Guid.NewGuid(), $"{command.FirstName} {command.LastName}", command.Email, command.Password, "user", DateTime.UtcNow, new List()); _mockUserRepository.Setup(x => x.GetAsync(command.Email)).ReturnsAsync(user); //Act and Assert diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/RefreshTokenServiceTests.cs b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/RefreshTokenServiceTests.cs index 2eb39f56f..07a57577d 100644 --- a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/RefreshTokenServiceTests.cs +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Application.UnitTests/Services/RefreshTokenServiceTests.cs @@ -87,7 +87,7 @@ public async Task UseAsync_WithValidToken_ReturnsAuthDto() //Arrange var refreshToken = "validToken"; var token = new RefreshToken(new AggregateId(), Guid.NewGuid(), refreshToken, DateTime.UtcNow); - var user = new User(Guid.NewGuid(), "test@gmail.com", "password", "user", DateTime.UtcNow, new List()); + var user = new User(Guid.NewGuid(), "name", "test@gmail.com", "password", "user", DateTime.UtcNow, new List()); _mockRefreshTokenRepository.Setup(x => x.GetAsync(refreshToken)).ReturnsAsync(token); _mockUserRepository.Setup(x => x.GetAsync(token.UserId)).ReturnsAsync(user); _mockJwtProvider.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>())) diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.csproj b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.csproj index 9e30bd5e3..1e10ffc17 100644 --- a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.csproj +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.csproj @@ -1,7 +1,7 @@ - + net8.0 disable false diff --git a/MiniSpace.Services.Posts/.gitignore b/MiniSpace.Services.Posts/.gitignore new file mode 100644 index 000000000..c1aafb511 --- /dev/null +++ b/MiniSpace.Services.Posts/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ \ No newline at end of file diff --git a/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln b/MiniSpace.Services.Posts/MiniSpace.Services.Posts.sln new file mode 100644 index 000000000..468fc6e92 --- /dev/null +++ b/MiniSpace.Services.Posts/MiniSpace.Services.Posts.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", "{16C8A8E2-B637-4A8B-828F-876523BCC79F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Api", "src\MiniSpace.Services.Posts.Api\MiniSpace.Services.Posts.Api.csproj", "{5D62C437-6137-4000-A90A-6549835F593B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Application", "src\MiniSpace.Services.Posts.Application\MiniSpace.Services.Posts.Application.csproj", "{A17406AF-E11D-421B-9913-9512BB9B3333}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Core", "src\MiniSpace.Services.Posts.Core\MiniSpace.Services.Posts.Core.csproj", "{0FD8C55D-00F5-4059-889A-D223C7D1275C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Posts.Infrastructure", "src\MiniSpace.Services.Posts.Infrastructure\MiniSpace.Services.Posts.Infrastructure.csproj", "{515F6D6B-FB63-40F7-8173-2B94031F29B1}" +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 + {5D62C437-6137-4000-A90A-6549835F593B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D62C437-6137-4000-A90A-6549835F593B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D62C437-6137-4000-A90A-6549835F593B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D62C437-6137-4000-A90A-6549835F593B}.Release|Any CPU.Build.0 = Release|Any CPU + {A17406AF-E11D-421B-9913-9512BB9B3333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A17406AF-E11D-421B-9913-9512BB9B3333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A17406AF-E11D-421B-9913-9512BB9B3333}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A17406AF-E11D-421B-9913-9512BB9B3333}.Release|Any CPU.Build.0 = Release|Any CPU + {0FD8C55D-00F5-4059-889A-D223C7D1275C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FD8C55D-00F5-4059-889A-D223C7D1275C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FD8C55D-00F5-4059-889A-D223C7D1275C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FD8C55D-00F5-4059-889A-D223C7D1275C}.Release|Any CPU.Build.0 = Release|Any CPU + {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {515F6D6B-FB63-40F7-8173-2B94031F29B1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5D62C437-6137-4000-A90A-6549835F593B} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {A17406AF-E11D-421B-9913-9512BB9B3333} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {0FD8C55D-00F5-4059-889A-D223C7D1275C} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + {515F6D6B-FB63-40F7-8173-2B94031F29B1} = {16C8A8E2-B637-4A8B-828F-876523BCC79F} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Posts/scripts/build.sh b/MiniSpace.Services.Posts/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Posts/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Posts/scripts/start.sh b/MiniSpace.Services.Posts/scripts/start.sh new file mode 100644 index 000000000..79c296d65 --- /dev/null +++ b/MiniSpace.Services.Posts/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Posts.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Posts/scripts/test.sh b/MiniSpace.Services.Posts/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Posts/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/MiniSpace.Services.Posts.Api.csproj b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/MiniSpace.Services.Posts.Api.csproj new file mode 100644 index 000000000..d04753593 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/MiniSpace.Services.Posts.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Posts.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs new file mode 100644 index 000000000..aa979769d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs @@ -0,0 +1,46 @@ +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 Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Posts.Application; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Infrastructure; + +namespace MiniSpace.Services.Posts.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() + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get>("posts") + .Put("posts/{postId}") + .Delete("posts/{postId}") + .Post("posts", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"posts/{cmd.PostId}")) + .Put("posts/{postId}/state/{state}", + afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) + )) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Properties/launchSettings.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Properties/launchSettings.json new file mode 100644 index 000000000..9712e690f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5013" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Students": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5013", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.Development.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.Development.json new file mode 100644 index 000000000..f3ee419db --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.json new file mode 100644 index 000000000..4d566948d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json new file mode 100644 index 000000000..b7da5b832 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Posts Service", + "service": "posts-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "posts-service", + "address": "docker.for.win.localhost", + "port": "5013", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "posts-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": "posts", + "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": "posts-service", + "seed": false + }, + "outbox": { + "enabled": true, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "posts-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": "posts" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "posts-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "posts:" + }, + "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": "posts-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "posts-service", + "commonName": "posts-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "posts-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ChangePostState.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ChangePostState.cs new file mode 100644 index 000000000..211e6572f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ChangePostState.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class ChangePostState : ICommand + { + public Guid PostId { get; } + public string State { get; } + public DateTime? PublishDate { get; } + + public ChangePostState(Guid postId, string state, DateTime? publishDate) + { + PostId = postId; + State = state; + PublishDate = publishDate; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs new file mode 100644 index 000000000..8312bdd00 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class CreatePost : ICommand + { + public Guid PostId { get; } + public Guid EventId { get; } + public Guid StudentId { get; } + public string TextContent { get; } + public string MediaContent { get; } + public string State { get; } + public DateTime? PublishDate { get; } + + public CreatePost(Guid postId, Guid eventId, Guid studentId, string textContent, + string mediaContent, string state, DateTime? publishDate) + { + PostId = postId == Guid.Empty ? Guid.NewGuid() : postId; + EventId = eventId; + StudentId = studentId; + TextContent = textContent; + MediaContent = mediaContent; + State = state; + PublishDate = publishDate; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/DeletePost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/DeletePost.cs new file mode 100644 index 000000000..8fab824c7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/DeletePost.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class DeletePost : ICommand + { + public Guid PostId { get; } + + public DeletePost(Guid postId) => PostId = postId; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ChangePostStateHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ChangePostStateHandler.cs new file mode 100644 index 000000000..c18744f10 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ChangePostStateHandler.cs @@ -0,0 +1,86 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Exceptions; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class ChangePostStateHandler : ICommandHandler + { + private readonly IPostRepository _postRepository; + private readonly IAppContext _appContext; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public ChangePostStateHandler(IPostRepository postRepository, IAppContext appContext, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _postRepository = postRepository; + _appContext = appContext; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(ChangePostState command, CancellationToken cancellationToken = default) + { + var post = await _postRepository.GetAsync(command.PostId); + if (post is null) + { + throw new PostNotFoundException(command.PostId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + { + throw new UnauthorizedPostAccessException(command.PostId, identity.Id); + } + + if (!Enum.TryParse(command.State, true, out var newState)) + { + throw new InvalidPostStateException(command.State); + } + + if (!identity.IsAdmin && post.State == State.Reported) + { + throw new UnauthorizedPostOperationException(command.PostId, identity.Id); + } + + if (post.State == newState && post.State != State.ToBePublished) + { + throw new PostStateAlreadySetException(post.Id, newState); + } + + var previousState = post.State.ToString().ToLowerInvariant(); + + switch (newState) + { + case State.ToBePublished: + post.SetToBePublished(command.PublishDate + ?? throw new PublishDateNullException(command.PostId, newState), + _dateTimeProvider.Now); + break; + case State.Published: + post.SetPublished(); + break; + case State.InDraft: + post.SetInDraft(); + break; + case State.Hidden: + post.SetHidden(); + break; + case State.Reported: + post.SetReported(); + break; + default: + throw new InvalidPostStateException(post.State.ToString().ToLowerInvariant()); + } + + await _postRepository.UpdateAsync(post); + + await _messageBroker.PublishAsync(new PostStateChanged(post.Id, command.State, previousState)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs new file mode 100644 index 000000000..8e42c2db3 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs @@ -0,0 +1,54 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Exceptions; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class CreatePostHandler : ICommandHandler + { + private readonly IPostRepository _postRepository; + private readonly IStudentRepository _studentRepository; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + + public CreatePostHandler(IPostRepository postRepository, IStudentRepository studentRepository, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + { + _postRepository = postRepository; + _studentRepository = studentRepository; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CreatePost command, CancellationToken cancellationToken = default) + { + if (!(await _studentRepository.ExistsAsync(command.StudentId))) + { + throw new StudentNotFoundException(command.StudentId); + } + + if (!Enum.TryParse(command.State, true, out var newState)) + { + throw new InvalidPostStateException(command.State); + } + + switch (newState) + { + case State.Hidden or State.Reported: + throw new NotAllowedPostStateException(command.PostId, newState); + case State.ToBePublished when command.PublishDate is null: + throw new PublishDateNullException(command.PostId, newState); + } + + var post = Post.Create(command.PostId, command.EventId, command.StudentId, command.TextContent, + command.MediaContent, _dateTimeProvider.Now, newState, command.PublishDate); + await _postRepository.AddAsync(post); + + await _messageBroker.PublishAsync(new PostCreated(command.PostId)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/DeletePostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/DeletePostHandler.cs new file mode 100644 index 000000000..5bfae6f12 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/DeletePostHandler.cs @@ -0,0 +1,48 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class DeletePostHandler : ICommandHandler + { + private readonly IPostRepository _postRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeletePostHandler(IPostRepository postRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _postRepository = postRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeletePost command, CancellationToken cancellationToken = default) + { + var post = await _postRepository.GetAsync(command.PostId); + if (post is null) + { + throw new PostNotFoundException(command.PostId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + { + throw new UnauthorizedPostAccessException(command.PostId, identity.Id); + } + + if (!identity.IsAdmin && post.State == State.Reported) + { + throw new UnauthorizedPostOperationException(command.PostId, identity.Id); + } + + await _postRepository.DeleteAsync(command.PostId); + + await _messageBroker.PublishAsync(new PostDeleted(command.PostId)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs new file mode 100644 index 000000000..7cd591fea --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs @@ -0,0 +1,49 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class UpdatePostHandler : ICommandHandler + { + private readonly IPostRepository _postRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public UpdatePostHandler(IPostRepository postRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _postRepository = postRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdatePost command, CancellationToken cancellationToken = default) + { + var post = await _postRepository.GetAsync(command.PostId); + if (post is null) + { + throw new PostNotFoundException(command.PostId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + { + throw new UnauthorizedPostAccessException(command.PostId, identity.Id); + } + + if (!identity.IsAdmin && post.State == State.Reported) + { + throw new UnauthorizedPostOperationException(command.PostId, identity.Id); + } + + post.Update(command.TextContent, command.MediaContent); + await _postRepository.UpdateAsync(post); + + await _messageBroker.PublishAsync(new PostUpdated(command.PostId)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostsStateHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostsStateHandler.cs new file mode 100644 index 000000000..03996fcb7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostsStateHandler.cs @@ -0,0 +1,32 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class UpdatePostsStateHandler : ICommandHandler + { + private IPostRepository _postRepository; + private IMessageBroker _messageBroker; + + public UpdatePostsStateHandler(IPostRepository postRepository, IMessageBroker messageBroker) + { + _postRepository = postRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdatePostsState command, CancellationToken cancellationToken) + { + var posts = (await _postRepository.GetToUpdateAsync()).ToList(); + foreach (var @post in posts) + { + if (@post.UpdateState(command.Now)) + await _postRepository.UpdateAsync(@post); + } + + await _messageBroker.PublishAsync(new PostsStateUpdated(command.Now)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs new file mode 100644 index 000000000..00f6d6f40 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class UpdatePost : ICommand + { + public Guid PostId { get; } + public string TextContent { get; } + public string MediaContent { get; } + + public UpdatePost(Guid postId, string textContent, string mediaContent) + { + PostId = postId; + TextContent = textContent; + MediaContent = mediaContent; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePostsState.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePostsState.cs new file mode 100644 index 000000000..a270a9ff7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePostsState.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class UpdatePostsState : ICommand + { + public DateTime Now { get; set; } + + public UpdatePostsState(DateTime now) + { + Now = now; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/ContractAttribute.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/ContractAttribute.cs new file mode 100644 index 000000000..8c548bcfa --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Posts.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs new file mode 100644 index 000000000..0773f6d4e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Application.Dto +{ + public class PostDto + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + public string TextContent { get; set; } + public string MediaContent { get; set; } + public string State { get; set; } + public DateTime? PublishDate { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 000000000..4d3252933 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.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 = default) + { + if (await _studentRepository.ExistsAsync(@event.StudentId)) + { + throw new StudentAlreadyAddedException(@event.StudentId); + } + + await _studentRepository.AddAsync(new Student(@event.StudentId, @event.FullName)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs new file mode 100644 index 000000000..b2a2514d7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs @@ -0,0 +1,26 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +{ + public class StudentDeletedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + + public StudentDeletedHandler(IStudentRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task HandleAsync(StudentDeleted @event, CancellationToken cancellationToken = default) + { + if (!(await _studentRepository.ExistsAsync(@event.StudentId))) + { + throw new StudentNotFoundException(@event.StudentId); + } + + await _studentRepository.DeleteAsync(@event.StudentId); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentCreated.cs new file mode 100644 index 000000000..f7d769110 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("students")] + public class StudentCreated : IEvent + { + public Guid StudentId { get; } + public string FullName { get; } + + public StudentCreated(Guid studentId, string fullName) + { + StudentId = studentId; + FullName = fullName; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs new file mode 100644 index 000000000..844641aa5 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("students")] + public class StudentDeleted : IEvent + { + public Guid StudentId { get; } + public string FullName { get; } + + public StudentDeleted(Guid studentId, string fullName) + { + StudentId = studentId; + FullName = fullName; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStarted.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStarted.cs new file mode 100644 index 000000000..274dd3598 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStarted.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostBackgroundWorkerStarted(string name) : IEvent + { + public string Name { get; set; } = name; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStopped.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStopped.cs new file mode 100644 index 000000000..570599956 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostBackgroundWorkerStopped.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostBackgroundWorkerStopped(string name) : IEvent + { + public string Name { get; set; } = name; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs new file mode 100644 index 000000000..6675de454 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostCreated : IEvent + { + public Guid PostId { get; } + + public PostCreated(Guid postId) + { + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostDeleted.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostDeleted.cs new file mode 100644 index 000000000..1b20677ad --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostDeleted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostDeleted : IEvent + { + public Guid PostId { get; } + + public PostDeleted(Guid postId) + { + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostStateChanged.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostStateChanged.cs new file mode 100644 index 000000000..623119e63 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostStateChanged.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostStateChanged : IEvent + { + public Guid PostId { get; } + public string CurrentState { get; } + public string PreviousState { get; } + + public PostStateChanged(Guid postId, string currentState, string previousState) + { + PostId = postId; + CurrentState = currentState; + PreviousState = previousState; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs new file mode 100644 index 000000000..f6471e54f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostUpdated : IEvent + { + public Guid PostId { get; } + + public PostUpdated(Guid postId) + { + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostsStateUpdated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostsStateUpdated.cs new file mode 100644 index 000000000..9fe0af0e1 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostsStateUpdated.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events +{ + public class PostsStateUpdated(DateTime updateDateTime) : IEvent + { + public DateTime UpdateDateTime { get; set; } = updateDateTime; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs new file mode 100644 index 000000000..8415fb2dc --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/ChangePostStateRejected.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events.Rejected +{ + public class ChangePostStateRejected : IRejectedEvent + { + public Guid PostId { get; } + public string State { get; } + public string Reason { get; } + public string Code { get; } + + public ChangePostStateRejected(Guid postId, string state, string reason, string code) + { + PostId = postId; + State = state; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs new file mode 100644 index 000000000..6bbd8a7eb --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/CreatePostRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events.Rejected +{ + public class CreatePostRejected : IRejectedEvent + { + public Guid PostId { get; } + public string Reason { get; } + public string Code { get; } + + public CreatePostRejected(Guid postId, string reason, string code) + { + PostId = postId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs new file mode 100644 index 000000000..93c3b9c60 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/DeletePostRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events.Rejected +{ + public class DeletePostRejected : IRejectedEvent + { + public Guid PostId { get; } + public string Reason { get; } + public string Code { get; } + + public DeletePostRejected(Guid postId, string reason, string code) + { + PostId = postId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs new file mode 100644 index 000000000..f3638136e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/Rejected/UpdatePostRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Events.Rejected +{ + public class UpdatePostRejected : IRejectedEvent + { + public Guid PostId { get; } + public string Reason { get; } + public string Code { get; } + + public UpdatePostRejected(Guid postId, string reason, string code) + { + PostId = postId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/AppException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..f38c214e5 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/PostNotFoundException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/PostNotFoundException.cs new file mode 100644 index 000000000..62a9a2371 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/PostNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class PostNotFoundException : AppException + { + public override string Code { get; } = "post_not_found"; + public Guid Id { get; } + + public PostNotFoundException(Guid id) : base($"Post with id: {id} was not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs new file mode 100644 index 000000000..af328da1b --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.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; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentNotFoundException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentNotFoundException.cs new file mode 100644 index 000000000..1f7d6b593 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class StudentNotFoundException : AppException + { + public override string Code { get; } = "student_not_found"; + public Guid Id { get; } + + public StudentNotFoundException(Guid id) : base($"Student with id: {id} was not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostAccessException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostAccessException.cs new file mode 100644 index 000000000..1505a7ac2 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostAccessException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class UnauthorizedPostAccessException : AppException + { + public override string Code { get; } = "unauthorized_post_access"; + public Guid PostId { get; } + public Guid UserId { get; } + + public UnauthorizedPostAccessException(Guid postId, Guid userId) + : base($"Unauthorized access to post with id: '{postId}' by user with id: '{userId}'.") + { + PostId = postId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostOperationException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostOperationException.cs new file mode 100644 index 000000000..b6c5c2690 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostOperationException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class UnauthorizedPostOperationException : AppException + { + public override string Code { get; } = "not_allowed_post_operation"; + public Guid PostId { get; } + public Guid UserId { get; } + + public UnauthorizedPostOperationException(Guid postId, Guid userId) + : base($"Not allowed operation on post with id: '{postId}' by user with id: '{userId}'.") + { + PostId = postId; + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs new file mode 100644 index 000000000..bca122302 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IAppContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IAppContext.cs new file mode 100644 index 000000000..db2bbe03e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Posts.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IIdentityContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IIdentityContext.cs new file mode 100644 index 000000000..6c3cc1963 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Posts.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; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/MiniSpace.Services.Posts.Application.csproj b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/MiniSpace.Services.Posts.Application.csproj new file mode 100644 index 000000000..c626fc4a6 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/MiniSpace.Services.Posts.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs new file mode 100644 index 000000000..1efeae6f9 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPosts.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + public class GetPosts : IQuery> + { + public Guid EventId { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..53672d14b --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Posts.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IEventMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..07815a803 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Core.Events; + +namespace MiniSpace.Services.Posts.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..8c4c1f5d2 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Posts.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateId.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..0e3d31afa --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Posts.Core.Exceptions; + +namespace MiniSpace.Services.Posts.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(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..0c232c68a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Posts.Core.Events; + +namespace MiniSpace.Services.Posts.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(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs new file mode 100644 index 000000000..fe5bbb306 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs @@ -0,0 +1,102 @@ +using MiniSpace.Services.Posts.Core.Exceptions; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class Post : AggregateRoot + { + public Guid EventId { get; private set; } + public Guid StudentId { get; private set; } + public string TextContent { get; private set; } + public string MediaContent { get; private set; } + public State State { get; private set; } + public DateTime? PublishDate { get; private set; } + public DateTime CreatedAt { get; private set; } + + public Post(Guid id, Guid eventId, Guid studentId, string textContent, + string mediaContent, DateTime createdAt, State state, DateTime? publishDate) + { + Id = id; + EventId = eventId; + StudentId = studentId; + TextContent = textContent; + MediaContent = mediaContent; + CreatedAt = createdAt; + State = state; + PublishDate = publishDate; + } + + public void SetToBePublished(DateTime publishDate, DateTime now) + { + CheckPublishDate(Id, State.ToBePublished, publishDate, now); + State = State.ToBePublished; + PublishDate = publishDate; + } + + public void SetPublished() + { + State = State.Published; + PublishDate = null; + } + + public void SetInDraft() + { + State = State.InDraft; + PublishDate = null; + } + + public void SetHidden() + { + State = State.Hidden; + PublishDate = null; + } + + public void SetReported() + { + State = State.Reported; + PublishDate = null; + } + + public bool UpdateState(DateTime now) + { + if (State == State.ToBePublished && PublishDate <= now) + { + SetPublished(); + return true; + } + + return false; + } + + public static Post Create(AggregateId id, Guid eventId, Guid studentId, string textContent, + string mediaContent, DateTime createdAt, State state, DateTime? publishDate) + { + CheckContent(id, textContent, mediaContent); + + return new Post(id, eventId, studentId, textContent, mediaContent, createdAt, state, publishDate); + } + + public void Update(string textContent, string mediaContent) + { + CheckContent(Id, textContent, mediaContent); + + TextContent = textContent; + MediaContent = mediaContent; + } + + private static void CheckContent(AggregateId id, string textContent, string mediaContent) + { + if (string.IsNullOrWhiteSpace(textContent) && string.IsNullOrWhiteSpace(mediaContent)) + { + throw new InvalidPostContentException(id); + } + } + + private static void CheckPublishDate(AggregateId id, State state, DateTime publishDate, DateTime now) + { + if (publishDate <= now) + { + throw new InvalidPostPublishDateException(id, state, publishDate, now); + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/State.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/State.cs new file mode 100644 index 000000000..816a39ef0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/State.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Posts.Core.Entities +{ + public enum State + { + ToBePublished, + Published, + InDraft, + Hidden, + Reported + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs new file mode 100644 index 000000000..cfd028ec7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class Student + { + public Guid Id { get; private set; } + public string FullName { get; private set; } + + public Student(Guid id, string fullName) + { + Id = id; + FullName = fullName; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..9bc3c601b --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Posts.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..8010b6a33 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..0c1199ec7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostContentException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostContentException.cs new file mode 100644 index 000000000..4af205297 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostContentException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class InvalidPostContentException : DomainException + { + public override string Code { get; } = "invalid_post_content"; + public Guid Id { get; } + + public InvalidPostContentException(Guid id) : base( + $"Post with id: {id} has invalid content.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostPublishDateException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostPublishDateException.cs new file mode 100644 index 000000000..d1f8b5508 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostPublishDateException.cs @@ -0,0 +1,23 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class InvalidPostPublishDateException : DomainException + { + public override string Code { get; } = "invalid_post_publish_date"; + public Guid Id { get; } + public State State { get; } + public DateTime PublishDate { get; } + public DateTime Now { get; } + + public InvalidPostPublishDateException(Guid id, State state, DateTime publishDate, DateTime now) : base( + $"Post with id: {id} has invalid publish date. Today date: " + + $"'{now}' must be older than publish date: '{publishDate}'.") + { + Id = id; + State = state; + PublishDate = publishDate; + Now = now; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostStateException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostStateException.cs new file mode 100644 index 000000000..f52973d05 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostStateException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class InvalidPostStateException : DomainException + { + public override string Code { get; } = "invalid_post_state"; + public string InvalidState { get; } + + public InvalidPostStateException(string invalidState) : base( + $"String: {invalidState} cannot be parsed to valid post state.") + { + InvalidState = invalidState; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/NotAllowedPostStateException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/NotAllowedPostStateException.cs new file mode 100644 index 000000000..daa1788cb --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/NotAllowedPostStateException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class NotAllowedPostStateException : DomainException + { + public override string Code { get; } = "not_allowed_post_state"; + public Guid Id { get; } + public State State { get; } + + public NotAllowedPostStateException(Guid id, State state) : base( + $"State: {state} is not allowed for post: {id}.") + { + Id = id; + State = state; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PostStateAlreadySetException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PostStateAlreadySetException.cs new file mode 100644 index 000000000..b8fb72c51 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PostStateAlreadySetException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class PostStateAlreadySetException : DomainException + { + public override string Code { get; } = "post_state_already_set"; + public Guid Id { get; } + public State State { get; } + + public PostStateAlreadySetException(Guid id, State state) : base( + $"Post: {id} has state already set to: {state}.") + { + Id = id; + State = state; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PublishDateNullException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PublishDateNullException.cs new file mode 100644 index 000000000..cefcc390f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/PublishDateNullException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class PublishDateNullException : DomainException + { + public override string Code { get; } = "publish_date_null"; + public Guid Id { get; } + public State State { get; } + + public PublishDateNullException(Guid id, State state) : base( + $"Publish date cannot be null for post: {id}.") + { + Id = id; + State = state; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/MiniSpace.Services.Posts.Core.csproj b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/MiniSpace.Services.Posts.Core.csproj new file mode 100644 index 000000000..4968dd9b1 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/MiniSpace.Services.Posts.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs new file mode 100644 index 000000000..5029a51a1 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs @@ -0,0 +1,13 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IPostRepository + { + Task GetAsync(Guid id); + Task> GetToUpdateAsync(); + Task AddAsync(Post post); + Task UpdateAsync(Post post); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..f5f9f020a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IStudentRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Student student); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..62d23d593 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Posts.Application; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..ac0ff8648 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Posts.Application; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..191f9e53d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..eed550c5a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Posts.Application; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..fe8411803 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.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.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..f98c19ea0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.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.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..06279da18 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,86 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Events.Rejected; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Core.Exceptions; + +namespace MiniSpace.Services.Posts.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + InvalidPostContentException ex => message switch + { + CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, + ex.Code), + UpdatePost _ => new UpdatePostRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + InvalidPostPublishDateException ex => message switch + { + ChangePostState _ => new ChangePostStateRejected(ex.Id, + ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), + _ => null, + }, + NotAllowedPostStateException ex => message switch + { + CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + PostNotFoundException ex => message switch + { + UpdatePost _ => new UpdatePostRejected(ex.Id, ex.Message, + ex.Code), + DeletePost _ => new DeletePostRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + PostStateAlreadySetException ex => message switch + { + ChangePostState _ => new ChangePostStateRejected(ex.Id, + ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), + _ => null, + }, + PublishDateNullException ex => message switch + { + CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, + ex.Code), + ChangePostState _ => new ChangePostStateRejected(ex.Id, + ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), + _ => null, + }, + StudentNotFoundException ex => message switch + { + CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + UnauthorizedPostAccessException ex => message switch + { + UpdatePost _ => new UpdatePostRejected(ex.PostId, ex.Message, + ex.Code), + DeletePost _ => new DeletePostRejected(ex.PostId, ex.Message, + ex.Code), + ChangePostState _ => new ChangePostStateRejected(ex.PostId, + "unknown", ex.Message, ex.Code), + _ => null, + }, + UnauthorizedPostOperationException ex => message switch + { + UpdatePost _ => new UpdatePostRejected(ex.PostId, ex.Message, + ex.Code), + DeletePost _ => new DeletePostRejected(ex.PostId, ex.Message, + ex.Code), + ChangePostState _ => new ChangePostStateRejected(ex.PostId, + "unknown", ex.Message, ex.Code), + _ => null, + }, + _ => null + }; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..70c051e7b --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Core.Exceptions; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs new file mode 100644 index 000000000..bcab93e0e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs @@ -0,0 +1,138 @@ +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.Posts.Application; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Events.External; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Contexts; +using MiniSpace.Services.Posts.Infrastructure.Decorators; +using MiniSpace.Services.Posts.Infrastructure.Exceptions; +using MiniSpace.Services.Posts.Infrastructure.Logging; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Services; +using MiniSpace.Services.Posts.Infrastructure.Services.Workers; + +namespace MiniSpace.Services.Posts.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + 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<>)); + builder.Services.AddHostedService(); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("students") + .AddMongoRepository("posts") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .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.Posts/src/MiniSpace.Services.Posts.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..a7cedd98d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Posts.Application; + +namespace MiniSpace.Services.Posts.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..5e5cd3f91 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Posts.Application.Commands; + +namespace MiniSpace.Services.Posts.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UpdatePost).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..806153d20 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,64 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Events.External; + +namespace MiniSpace.Services.Posts.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(CreatePost), new HandlerLogTemplate + { + After = "Created the post with id: {PostId}." + } + }, + { + typeof(UpdatePost), new HandlerLogTemplate + { + After = "Updated the post with id: {PostId}." + } + }, + { + typeof(DeletePost), new HandlerLogTemplate + { + After = "Deleted the post with id: {PostId}." + } + }, + { + typeof(ChangePostState), new HandlerLogTemplate + { + After = "Changed a post with id: {PostId} state to: {State}." + } + }, + { + typeof(StudentCreated), new HandlerLogTemplate + { + After = "Created a new student with id: {StudentId}." + } + }, + { + typeof(StudentDeleted), new HandlerLogTemplate + { + After = "Deleted a student with id: {StudentId}." + } + }, + { + typeof(PostsStateUpdated), + new HandlerLogTemplate + { + After = "Updated state of posts at: {Now}." + } + }, + }; + + 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.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj new file mode 100644 index 000000000..34640728d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..5cb5b168b --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,48 @@ +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Post AsEntity(this PostDocument document) + => new Post(document.Id, document.EventId, document.StudentId, document.TextContent, + document.MediaContent, document.CreatedAt, document.State, document.PublishDate); + + public static PostDocument AsDocument(this Post entity) + => new PostDocument() + { + Id = entity.Id, + EventId = entity.EventId, + StudentId = entity.StudentId, + TextContent = entity.TextContent, + MediaContent = entity.MediaContent, + CreatedAt = entity.CreatedAt, + State = entity.State, + PublishDate = entity.PublishDate + }; + + public static PostDto AsDto(this PostDocument document) + => new PostDto() + { + Id = document.Id, + EventId = document.EventId, + StudentId = document.StudentId, + TextContent = document.TextContent, + MediaContent = document.MediaContent, + CreatedAt = document.CreatedAt, + State = document.State.ToString().ToLowerInvariant(), + PublishDate = document.PublishDate + }; + + public static Student AsEntity(this StudentDocument document) + => new Student(document.Id, document.FullName); + + public static StudentDocument AsDocument(this Student entity) + => new StudentDocument + { + Id = entity.Id, + FullName = entity.FullName + }; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs new file mode 100644 index 000000000..efe647ad0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs @@ -0,0 +1,17 @@ +using Convey.Types; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class PostDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + public string TextContent { get; set; } + public string MediaContent { get; set; } + public State State { get; set; } + public DateTime? PublishDate { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..b873cc56c --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,10 @@ +using Convey.Types; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class StudentDocument : IIdentifiable + { + public Guid Id { get; set; } + public string FullName { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs new file mode 100644 index 000000000..7420c52d6 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostsHandler.cs @@ -0,0 +1,29 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers +{ + public class GetPostsHandler : IQueryHandler> + { + private readonly IMongoRepository _postRepository; + + public GetPostsHandler(IMongoRepository postRepository) + { + _postRepository = postRepository; + } + + public async Task> HandleAsync(GetPosts query, CancellationToken cancellationToken) + { + var documents = _postRepository.Collection.AsQueryable(); + documents = documents.Where(p => p.EventId == query.EventId); + + var posts = await documents.ToListAsync(); + return posts.Select(p => p.AsDto()); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs new file mode 100644 index 000000000..9a36f751a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs @@ -0,0 +1,43 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Posts.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 async Task> GetToUpdateAsync() + { + var posts = _repository.Collection.AsQueryable(); + var postsToUpdate = await posts.Where(e + => e.State == State.ToBePublished || e.State == State.Published).ToListAsync(); + return postsToUpdate.Select(e => e.AsEntity()); + } + + public Task AddAsync(Post post) + => _repository.AddAsync(post.AsDocument()); + + public Task UpdateAsync(Post post) + => _repository.UpdateAsync(post.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 000000000..4f4e84eaf --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,33 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Posts.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()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..3b55bdb20 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Posts.Application.Services; + +namespace MiniSpace.Services.Posts.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..fe9f6b6d8 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core; +using MiniSpace.Services.Posts.Core.Events; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..83b388249 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.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.Posts.Application.Services; + +namespace MiniSpace.Services.Posts.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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs new file mode 100644 index 000000000..b469e0263 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Workers/PostStateUpdaterWorker.cs @@ -0,0 +1,42 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Hosting; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Events; +using MiniSpace.Services.Posts.Application.Services; + +namespace MiniSpace.Services.Posts.Infrastructure.Services.Workers +{ + public class PostStateUpdaterWorker: BackgroundService + { + private readonly IMessageBroker _messageBroker; + private readonly ICommandDispatcher _commandDispatcher; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(10); + + public PostStateUpdaterWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, + IDateTimeProvider dateTimeProvider) + { + _messageBroker = messageBroker; + _commandDispatcher = commandDispatcher; + _dateTimeProvider = dateTimeProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _messageBroker.PublishAsync(new PostBackgroundWorkerStarted("state_updater")); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await _commandDispatcher.SendAsync(new UpdatePostsState(_dateTimeProvider.Now), stoppingToken); + await Task.Delay(_updateInterval, stoppingToken); + } + catch (TaskCanceledException) + { + await _messageBroker.PublishAsync(new PostBackgroundWorkerStopped("state_updater")); + return; + } + } + } + } +} diff --git a/MiniSpace.Services.Students/.gitignore b/MiniSpace.Services.Students/.gitignore new file mode 100644 index 000000000..64def56a6 --- /dev/null +++ b/MiniSpace.Services.Students/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Students/Dockerfile b/MiniSpace.Services.Students/Dockerfile new file mode 100644 index 000000000..d29475e3d --- /dev/null +++ b/MiniSpace.Services.Students/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Students.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Students.Api.dll"] \ No newline at end of file diff --git a/MiniSpace.Services.Students/LICENSE b/MiniSpace.Services.Students/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.Students/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.Students/MiniSpace.Services.Students.sln b/MiniSpace.Services.Students/MiniSpace.Services.Students.sln new file mode 100644 index 000000000..ff5faab9b --- /dev/null +++ b/MiniSpace.Services.Students/MiniSpace.Services.Students.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", "{0E976732-0CD9-4D2E-B989-998B124073BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Api", "src\MiniSpace.Services.Students.Api\MiniSpace.Services.Students.Api.csproj", "{D915BFB5-D2D4-44C8-A3A3-379419079F06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Application", "src\MiniSpace.Services.Students.Application\MiniSpace.Services.Students.Application.csproj", "{97B39658-9B33-4124-90E5-102FDA7D3733}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Core", "src\MiniSpace.Services.Students.Core\MiniSpace.Services.Students.Core.csproj", "{C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Infrastructure", "src\MiniSpace.Services.Students.Infrastructure\MiniSpace.Services.Students.Infrastructure.csproj", "{B2997BE8-0CE3-45DD-98E9-80599B070C25}" +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 + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.Build.0 = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.Build.0 = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.Build.0 = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {97B39658-9B33-4124-90E5-102FDA7D3733} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {B2997BE8-0CE3-45DD-98E9-80599B070C25} = {0E976732-0CD9-4D2E-B989-998B124073BA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Students/scripts/build.sh b/MiniSpace.Services.Students/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Students/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Students/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Students/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..4d791f9ef --- /dev/null +++ b/MiniSpace.Services.Students/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.students:latest . + +docker tag minispace.services.students:latest adrianvsaint/minispace.services.students:latest + +docker push adrianvsaint/minispace.services.students:latest diff --git a/MiniSpace.Services.Students/scripts/start.sh b/MiniSpace.Services.Students/scripts/start.sh new file mode 100644 index 000000000..29eeea1f8 --- /dev/null +++ b/MiniSpace.Services.Students/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Students.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Students/scripts/test.sh b/MiniSpace.Services.Students/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Students/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.csproj b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.csproj new file mode 100644 index 000000000..0eb5a6f59 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Students.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.sln b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.sln new file mode 100644 index 000000000..ace8fdbc6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/MiniSpace.Services.Students.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Students.Api", "MiniSpace.Services.Students.Api.csproj", "{5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {284B46A7-2ABB-4E20-8B52-4291CC46AE00} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs new file mode 100644 index 000000000..38d3b7d0d --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs @@ -0,0 +1,47 @@ +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 Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Students.Application; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure; + +namespace MiniSpace.Services.Students.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() + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get>("students") + .Get("students/{studentId}") + .Put("students/{studentId}") + .Delete("students/{studentId}") + .Post("students", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"students/{cmd.StudentId}")) + .Put("students/{studentId}/state/{state}", + afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) + .Get("students/{studentId}/events"))) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Properties/launchSettings.json b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Properties/launchSettings.json new file mode 100644 index 000000000..07369530a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5007" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Students": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5007", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.Development.json b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.Development.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.docker.json b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.docker.json new file mode 100644 index 000000000..ed2cb6e78 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Students Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "students-service", + "address": "students-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "students-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": "students", + "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": "students-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "students-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": "students" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "students-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "students:" + }, + "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.Students/src/MiniSpace.Services.Students.Api/appsettings.json b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.local.json b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.local.json new file mode 100644 index 000000000..78aa299b4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Students Service", + "service": "students-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "students-service", + "address": "docker.for.win.localhost", + "port": "5007", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "students-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": "students", + "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": "students-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "students-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": "students" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "students-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "students:" + }, + "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": "students-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "students-service", + "commonName": "students-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "students-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ChangeStudentState.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ChangeStudentState.cs new file mode 100644 index 000000000..4dcad43d7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ChangeStudentState.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class ChangeStudentState : ICommand + { + public Guid StudentId { get; } + public string State { get; } + + public ChangeStudentState(Guid studentId, string state) + { + StudentId = studentId; + State = state; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs new file mode 100644 index 000000000..11713de39 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class CompleteStudentRegistration : ICommand + { + public Guid StudentId { get; } + public string ProfileImage { get; } + public string Description { get; } + public DateTime DateOfBirth { get; } + public bool EmailNotifications { get; } + + public CompleteStudentRegistration(Guid studentId, string profileImage, + string description, DateTime dateOfBirth, bool emailNotifications) + { + StudentId = studentId; + ProfileImage = profileImage; + Description = description; + DateOfBirth = dateOfBirth; + EmailNotifications = emailNotifications; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/DeleteStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/DeleteStudent.cs new file mode 100644 index 000000000..9e0de1ddd --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/DeleteStudent.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class DeleteStudent : ICommand + { + public Guid StudentId { get; } + + public DeleteStudent(Guid studentId) => StudentId = studentId; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ChangeStudentStateHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ChangeStudentStateHandler.cs new file mode 100644 index 000000000..c72d19c8a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ChangeStudentStateHandler.cs @@ -0,0 +1,63 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Exceptions; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class ChangeStudentStateHandler : ICommandHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public ChangeStudentStateHandler(IStudentRepository studentRepository, IEventMapper eventMapper, + IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(ChangeStudentState command, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + if (!Enum.TryParse(command.State, true, out var state)) + { + throw new CannotChangeStudentStateException(student.Id, State.Unknown); + } + + if (student.State == state) + { + throw new StudentStateAlreadySetException(student.Id, state); + } + + switch (state) + { + case State.Incomplete: + student.SetIncomplete(); + break; + case State.Valid: + student.SetValid(); + break; + case State.Banned: + student.SetBanned(); + break; + default: + throw new CannotChangeStudentStateException(student.Id, state); + } + + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/CompleteStudentRegistrationHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/CompleteStudentRegistrationHandler.cs new file mode 100644 index 000000000..81c35c177 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/CompleteStudentRegistrationHandler.cs @@ -0,0 +1,46 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Exceptions; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class CompleteStudentRegistrationHandler : ICommandHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public CompleteStudentRegistrationHandler(IStudentRepository studentRepository, + IDateTimeProvider dateTimeProvider, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _dateTimeProvider = dateTimeProvider; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CompleteStudentRegistration command, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + if (student.State is Core.Entities.State.Valid) + { + throw new StudentAlreadyRegisteredException(command.StudentId); + } + + student.CompleteRegistration(command.ProfileImage, command.Description, + command.DateOfBirth, _dateTimeProvider.Now, command.EmailNotifications); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/DeleteStudentHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/DeleteStudentHandler.cs new file mode 100644 index 000000000..0af6ac753 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/DeleteStudentHandler.cs @@ -0,0 +1,42 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class DeleteStudentHandler : ICommandHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteStudentHandler(IStudentRepository studentRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteStudent command, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != student.Id && !identity.IsAdmin) + { + throw new UnauthorizedStudentAccessException(command.StudentId, identity.Id); + } + + await _studentRepository.DeleteAsync(command.StudentId); + + await _messageBroker.PublishAsync(new StudentDeleted(command.StudentId)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs new file mode 100644 index 000000000..a2ea9dd8a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs @@ -0,0 +1,45 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class UpdateStudentHandler : ICommandHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IAppContext _appContext; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UpdateStudentHandler(IStudentRepository studentRepository, IAppContext appContext, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _appContext = appContext; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateStudent command, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != student.Id && !identity.IsAdmin) + { + throw new UnauthorizedStudentAccessException(command.StudentId, identity.Id); + } + + student.Update(command.ProfileImage, command.Description, command.EmailNotifications); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs new file mode 100644 index 000000000..d76bb3ba2 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UpdateStudent : ICommand + { + public Guid StudentId { get; } + public string ProfileImage { get; } + public string Description { get; } + public bool EmailNotifications { get; } + + public UpdateStudent(Guid studentId, string profileImage, string description, bool emailNotifications) + { + StudentId = studentId; + ProfileImage = profileImage; + Description = description; + EmailNotifications = emailNotifications; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/ContractAttribute.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/ContractAttribute.cs new file mode 100644 index 000000000..de88f60d1 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Students.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs new file mode 100644 index 000000000..0d934580e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs @@ -0,0 +1,21 @@ +namespace MiniSpace.Services.Students.Application.Dto +{ + public class StudentDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool CanBeOrganizer { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentEventsDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentEventsDto.cs new file mode 100644 index 000000000..915057089 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentEventsDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Students.Application.Dto +{ + public class StudentEventsDto + { + public Guid StudentId { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs new file mode 100644 index 000000000..8fb7eec02 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class OrganizerRightsGrantedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public OrganizerRightsGrantedHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentShowedInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.GrantOrganizerRights(); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs new file mode 100644 index 000000000..e53bcc20a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class OrganizerRightsRevokedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public OrganizerRightsRevokedHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentShowedInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.RevokeOrganizerRights(); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs new file mode 100644 index 000000000..0e6202eeb --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs @@ -0,0 +1,43 @@ +using Convey.CQRS.Events; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class SignedUpHandler : IEventHandler + { + private const string RequiredRole = "user"; + private readonly IStudentRepository _studentRepository; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ILogger _logger; + + public SignedUpHandler(IStudentRepository studentRepository, IDateTimeProvider dateTimeProvider, + ILogger logger) + { + _studentRepository = studentRepository; + _dateTimeProvider = dateTimeProvider; + _logger = logger; + } + + public async Task HandleAsync(SignedUp @event, CancellationToken cancellationToken = default) + { + if (@event.Role != RequiredRole) + { + throw new InvalidRoleException(@event.UserId, @event.Role, RequiredRole); + } + + var student = await _studentRepository.GetAsync(@event.UserId); + if (student is not null) + { + throw new StudentAlreadyCreatedException(student.Id); + } + + var newStudent = new Student(@event.UserId, @event.FirstName, @event.LastName, + @event.Email, _dateTimeProvider.Now); + await _studentRepository.AddAsync(newStudent); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentShowedInterestInEventHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentShowedInterestInEventHandler.cs new file mode 100644 index 000000000..318d92bda --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentShowedInterestInEventHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class StudentShowedInterestInEventHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public StudentShowedInterestInEventHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentShowedInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.AddInterestedInEvent(@event.EventId); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentSignedUpToEventHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentSignedUpToEventHandler.cs new file mode 100644 index 000000000..213e6f374 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentSignedUpToEventHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class StudentSignedUpToEventHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public StudentSignedUpToEventHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentSignedUpToEvent studentSignedUpToEvent, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(studentSignedUpToEvent.StudentId); + if (student is null) + { + throw new StudentNotFoundException(studentSignedUpToEvent.StudentId); + } + + student.AddSignedUpEvent(studentSignedUpToEvent.EventId); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserBannedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserBannedHandler.cs new file mode 100644 index 000000000..84f82af26 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserBannedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class UserBannedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UserBannedHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentShowedInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.Ban(); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserUnbannedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserUnbannedHandler.cs new file mode 100644 index 000000000..6776565b7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/UserUnbannedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class UserUnbannedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UserUnbannedHandler(IStudentRepository studentRepository, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(StudentShowedInterestInEvent @event, CancellationToken cancellationToken) + { + var student = await _studentRepository.GetAsync(@event.StudentId); + if (student is null) + { + throw new StudentNotFoundException(@event.StudentId); + } + + student.Unban(); + await _studentRepository.UpdateAsync(student); + + var events = _eventMapper.MapAll(student.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs new file mode 100644 index 000000000..e62aaecdb --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("identity")] + public class OrganizerRightsGranted : IEvent + { + public Guid UserId { get; } + public OrganizerRightsGranted(Guid userId) + { + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs new file mode 100644 index 000000000..eb0f57953 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("identity")] + public class OrganizerRightsRevoked : IEvent + { + public Guid UserId { get; } + public OrganizerRightsRevoked(Guid userId) + { + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/SignedUp.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/SignedUp.cs new file mode 100644 index 000000000..8b2d057bc --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/SignedUp.cs @@ -0,0 +1,24 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("identity")] + public class SignedUp : IEvent + { + public Guid UserId { get; } + public string FirstName { get; } + public string LastName { get; } + public string Email { get; } + public string Role { get; } + + public SignedUp(Guid userId, string firstName, string lastName, string email, string role) + { + UserId = userId; + FirstName = firstName; + LastName = lastName; + Email = email; + Role = role; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentShowedInterestInEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentShowedInterestInEvent.cs new file mode 100644 index 000000000..c42e2543c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentShowedInterestInEvent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("events")] + public class StudentShowedInterestInEvent : IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentShowedInterestInEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentSignedUpToEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentSignedUpToEvent.cs new file mode 100644 index 000000000..39d4f18db --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentSignedUpToEvent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("events")] + public class StudentSignedUpToEvent : IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentSignedUpToEvent(Guid eventId, Guid studentId) + { + EventId = eventId; + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserBanned.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserBanned.cs new file mode 100644 index 000000000..c090e204f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserBanned.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("identity")] + public class UserBanned(Guid userId) : IEvent + { + public Guid UserId { get; } = userId; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserUnbanned.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserUnbanned.cs new file mode 100644 index 000000000..967db645c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/UserUnbanned.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("identity")] + public class UserUnbanned(Guid userId) : IEvent + { + public Guid UserId { get; } = userId; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/ChangeStudentStateRejected.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/ChangeStudentStateRejected.cs new file mode 100644 index 000000000..6bd416448 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/ChangeStudentStateRejected.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events.Rejected +{ + public class ChangeStudentStateRejected : IRejectedEvent + { + public Guid StudentId { get; } + public string State { get; } + public string Reason { get; } + public string Code { get; } + + public ChangeStudentStateRejected(Guid studentId, string state, string reason, string code) + { + StudentId = studentId; + State = state; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CompleteStudentRegistrationRejected.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CompleteStudentRegistrationRejected.cs new file mode 100644 index 000000000..4ae056b5b --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CompleteStudentRegistrationRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Commands; + +namespace MiniSpace.Services.Students.Application.Events.Rejected +{ + public class CompleteStudentRegistrationRejected : IRejectedEvent + { + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public CompleteStudentRegistrationRejected(Guid studentId, string reason, string code) + { + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CreateStudentRejected.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CreateStudentRejected.cs new file mode 100644 index 000000000..4a021ba76 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/CreateStudentRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events.Rejected +{ + public class CreateStudentRejected : IRejectedEvent + { + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public CreateStudentRejected(Guid studentId, string reason, string code) + { + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/DeleteStudentRejected.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/DeleteStudentRejected.cs new file mode 100644 index 000000000..86cd76024 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/DeleteStudentRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events.Rejected +{ + public class DeleteStudentRejected : IRejectedEvent + { + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public DeleteStudentRejected(Guid studentId, string reason, string code) + { + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/UpdateStudentRejected.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/UpdateStudentRejected.cs new file mode 100644 index 000000000..54992d8d5 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/Rejected/UpdateStudentRejected.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events.Rejected +{ + public class UpdateStudentRejected : IRejectedEvent + { + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public UpdateStudentRejected(Guid studentId, string reason, string code) + { + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs new file mode 100644 index 000000000..816ea28bd --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class StudentCreated : IEvent + { + public Guid StudentId { get; } + + public StudentCreated(Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentDeleted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentDeleted.cs new file mode 100644 index 000000000..90c1a43ef --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentDeleted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class StudentDeleted : IEvent + { + public Guid StudentId { get; } + + public StudentDeleted(Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentStateChanged.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentStateChanged.cs new file mode 100644 index 000000000..d548e727e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentStateChanged.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class StudentStateChanged : IEvent + { + public Guid StudentId { get; } + public string CurrentState { get; } + public string PreviousState { get; } + + public StudentStateChanged(Guid studentId, string currentState, string previousState) + { + StudentId = studentId; + CurrentState = currentState; + PreviousState = previousState; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs new file mode 100644 index 000000000..a13bbdb57 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class StudentUpdated : IEvent + { + public Guid StudentId { get; } + + public StudentUpdated(Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/AppException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..b10ae33e3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/InvalidRoleException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/InvalidRoleException.cs new file mode 100644 index 000000000..b011bac43 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/InvalidRoleException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class InvalidRoleException : AppException + { + public override string Code { get; } = "invalid_role"; + + public InvalidRoleException(Guid userId, string role, string requiredRole) + : base($"Student account will not be created for the user with id: {userId} " + + $"due to the invalid role: {role} (required: {requiredRole}).") + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyCreatedException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyCreatedException.cs new file mode 100644 index 000000000..d62ec5a77 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyCreatedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class StudentAlreadyCreatedException : AppException + { + public override string Code { get; } = "student_already_created"; + public Guid StudentId { get; } + + public StudentAlreadyCreatedException(Guid studentId) + : base($"Student with id: {studentId} was already created.") + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyRegisteredException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyRegisteredException.cs new file mode 100644 index 000000000..5089aca57 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentAlreadyRegisteredException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class StudentAlreadyRegisteredException : AppException + { + public override string Code { get; } = "student_already_registered"; + public Guid Id { get; } + + public StudentAlreadyRegisteredException(Guid id) + : base($"Student with id: {id} has already been registered.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentNotFoundException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentNotFoundException.cs new file mode 100644 index 000000000..526707d29 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/StudentNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class StudentNotFoundException : AppException + { + public override string Code { get; } = "student_not_found"; + public Guid Id { get; } + + public StudentNotFoundException(Guid id) : base($"Student with id: {id} was not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UnauthorizedStudentAccessException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UnauthorizedStudentAccessException.cs new file mode 100644 index 000000000..7f118f210 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UnauthorizedStudentAccessException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UnauthorizedStudentAccessException : AppException + { + public override string Code { get; } = "unauthorized_student_access"; + public Guid StudentId { get; } + public Guid UserId { get; } + + public UnauthorizedStudentAccessException(Guid studentId, Guid userId) + : base($"Unauthorized access to student with id: '{studentId}' by user with id: '{userId}'.") + { + StudentId = studentId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Extensions.cs new file mode 100644 index 000000000..26796b5dd --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IAppContext.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IAppContext.cs new file mode 100644 index 000000000..6c368fc6d --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Students.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IIdentityContext.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IIdentityContext.cs new file mode 100644 index 000000000..58696f4bd --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/IIdentityContext.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Students.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.csproj b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.csproj new file mode 100644 index 000000000..fdf5b09ed --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudent.cs new file mode 100644 index 000000000..d646c94f6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudent.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetStudent : IQuery + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentEvents.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentEvents.cs new file mode 100644 index 000000000..0ddaf8d65 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentEvents.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetStudentEvents: IQuery + { + public Guid StudentId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudents.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudents.cs new file mode 100644 index 000000000..611b134b0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudents.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetStudents : IQuery> + { + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..b21e1284a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Students.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IEventMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..b945e8700 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..857148c25 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Students.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateId.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..4ce6d0442 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Students.Core.Exceptions; + +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..6a9ee8aff --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Core/Entities/State.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/State.cs new file mode 100644 index 000000000..d496907e8 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/State.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum State + { + Unknown, + Valid, + Incomplete, + Banned + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs new file mode 100644 index 000000000..69b585227 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -0,0 +1,171 @@ +using MiniSpace.Services.Students.Core.Events; +using MiniSpace.Services.Students.Core.Exceptions; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class Student : AggregateRoot + { + private ISet _interestedInEvents = new HashSet(); + private ISet _signedUpEvents = new HashSet(); + + public string Email { get; private set; } + public string FirstName { get; private set; } + public string LastName { get; private set; } + public string FullName => $"{FirstName} {LastName}"; + public int NumberOfFriends { get; private set; } + public string ProfileImage { get; private set; } + public string Description { get; private set; } + public DateTime? DateOfBirth { get; private set; } + public bool EmailNotifications { get; private set; } + public bool IsBanned { get; private set; } + public bool CanBeOrganizer { get; private set; } + public State State { get; private set; } + public DateTime CreatedAt { get; private set; } + + public IEnumerable InterestedInEvents + { + get => _interestedInEvents; + set => _interestedInEvents = new HashSet(value); + } + public IEnumerable SignedUpEvents + { + get => _signedUpEvents; + set => _signedUpEvents = new HashSet(value); + } + + public Student(Guid id, string firstName, string lastName, string email, DateTime createdAt) + : this(id, email, createdAt, firstName, lastName, 0, string.Empty, string.Empty, null, + false, false, true, State.Incomplete, Enumerable.Empty(), Enumerable.Empty()) + { + CheckFullName(firstName, lastName); + } + + public Student(Guid id, string email, DateTime createdAt, string firstName, string lastName, + int numberOfFriends, string profileImage, string description, DateTime? dateOfBirth, + bool emailNotifications, bool isBanned, bool canBeOrganizer, State state, + IEnumerable interestedInEvents = null, IEnumerable signedUpEvents = null) + { + Id = id; + Email = email; + CreatedAt = createdAt; + FirstName = firstName; + LastName = lastName; + NumberOfFriends = numberOfFriends; + ProfileImage = profileImage; + Description = description; + DateOfBirth = dateOfBirth; + EmailNotifications = emailNotifications; + IsBanned = isBanned; + CanBeOrganizer = canBeOrganizer; + State = state; + InterestedInEvents = interestedInEvents ?? Enumerable.Empty(); + SignedUpEvents = signedUpEvents ?? Enumerable.Empty(); + } + + public void SetIncomplete() => SetState(State.Incomplete); + public void SetValid() => SetState(State.Valid); + public void SetBanned() => SetState(State.Banned); + + private void SetState(State state) + { + var previousState = State; + State = state; + AddEvent(new StudentStateChanged(this, previousState)); + } + + public void CompleteRegistration(string profileImage, string description, + DateTime dateOfBirth, DateTime now, bool emailNotifications) + { + CheckProfileImage(profileImage); + CheckDescription(description); + CheckDateOfBirth(dateOfBirth, now); + + if (State != State.Incomplete) + { + throw new CannotChangeStudentStateException(Id, State); + } + + ProfileImage = profileImage; + Description = description; + DateOfBirth = dateOfBirth; + EmailNotifications = emailNotifications; + + State = State.Valid; + AddEvent(new StudentRegistrationCompleted(this)); + } + + public void Update(string profileImage, string description, bool emailNotifications) + { + CheckProfileImage(profileImage); + CheckDescription(description); + + if (State != State.Valid) + { + throw new CannotUpdateStudentException(Id); + } + + ProfileImage = profileImage; + Description = description; + EmailNotifications = emailNotifications; + + AddEvent(new StudentUpdated(this)); + } + + private void CheckFullName(string firstName, string lastName) + { + if (string.IsNullOrWhiteSpace(firstName) || string.IsNullOrWhiteSpace(lastName)) + { + throw new InvalidStudentFullNameException(Id, $"{firstName} {lastName}"); + } + } + + private void CheckProfileImage(string profileImage) + { + if (string.IsNullOrWhiteSpace(profileImage)) + { + throw new InvalidStudentProfileImageException(Id, profileImage); + } + } + + private void CheckDescription(string description) + { + if (string.IsNullOrWhiteSpace(description)) + { + throw new InvalidStudentDescriptionException(Id, description); + } + } + + private void CheckDateOfBirth(DateTime dateOfBirth, DateTime now) + { + if (dateOfBirth >= now) + { + throw new InvalidStudentDateOfBirthException(Id, dateOfBirth, now); + } + } + + public void AddInterestedInEvent(Guid eventId) + { + if (eventId == Guid.Empty) + { + return; + } + + _interestedInEvents.Add(eventId); + } + + public void AddSignedUpEvent(Guid eventId) + { + if (eventId == Guid.Empty) + { + return; + } + + _signedUpEvents.Add(eventId); + } + + public void Ban() => IsBanned = true; + public void Unban() => IsBanned = false; + public void GrantOrganizerRights() => CanBeOrganizer = true; + public void RevokeOrganizerRights() => CanBeOrganizer = false; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..76ca4d90c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Students.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentRegistrationCompleted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentRegistrationCompleted.cs new file mode 100644 index 000000000..1d7c57099 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentRegistrationCompleted.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class StudentRegistrationCompleted : IDomainEvent + { + public Student Student { get; } + + public StudentRegistrationCompleted(Student student) + { + Student = student; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentStateChanged.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentStateChanged.cs new file mode 100644 index 000000000..9701a6d9e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentStateChanged.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class StudentStateChanged : IDomainEvent + { + public Student Student { get; } + public State PreviousState { get; } + + public StudentStateChanged(Student student, State previousState) + { + Student = student; + PreviousState = previousState; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentUpdated.cs new file mode 100644 index 000000000..6c12cd8a3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentUpdated.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class StudentUpdated : IDomainEvent + { + public Student Student { get; } + + public StudentUpdated(Student student) + { + Student = student; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotChangeStudentStateException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotChangeStudentStateException.cs new file mode 100644 index 000000000..d217726be --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotChangeStudentStateException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class CannotChangeStudentStateException : DomainException + { + public override string Code { get; } = "cannot_change_student_state"; + public Guid Id { get; } + public State State { get; } + + public CannotChangeStudentStateException(Guid id, State state) : base( + $"Cannot change student: {id} state to: {state}.") + { + Id = id; + State = state; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotUpdateStudentException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotUpdateStudentException.cs new file mode 100644 index 000000000..14bd91686 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/CannotUpdateStudentException.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class CannotUpdateStudentException : DomainException + { + public override string Code { get; } = "cannot_update_student"; + public Guid Id { get; } + + public CannotUpdateStudentException(Guid id) : base( + $"Cannot update student: {id}.") + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..806c122c3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..08175d284 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDateOfBirthException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDateOfBirthException.cs new file mode 100644 index 000000000..38a6801f0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDateOfBirthException.cs @@ -0,0 +1,19 @@ +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class InvalidStudentDateOfBirthException : DomainException + { + public override string Code { get; } = "invalid_student_date_of_birth"; + public Guid Id; + public DateTime DateOfBirth { get; } + public DateTime Now { get; } + + public InvalidStudentDateOfBirthException(Guid id, DateTime dateOfBirth, DateTime now) : base( + $"Student with id: {id} has invalid date of birth. Today date: " + + $"'{now}' must be greater than date of birth: '{dateOfBirth}'.") + { + Id = id; + DateOfBirth = dateOfBirth; + Now = now; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDescriptionException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDescriptionException.cs new file mode 100644 index 000000000..81baff0b5 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentDescriptionException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class InvalidStudentDescriptionException : DomainException + { + public override string Code { get; } = "invalid_student_description"; + public Guid Id { get; } + public string Description { get; } + + public InvalidStudentDescriptionException(Guid id, string description) : base( + $"Student with id: {id} has invalid description.") + { + Id = id; + Description = description; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentFullNameException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentFullNameException.cs new file mode 100644 index 000000000..3941b195e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentFullNameException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class InvalidStudentFullNameException : DomainException + { + public override string Code { get; } = "invalid_student_full_name"; + public Guid Id { get; } + public string FullName { get; } + + public InvalidStudentFullNameException(Guid id, string fullName) : base( + $"Student with id: {id} has invalid full name.") + { + Id = id; + FullName = fullName; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentProfileImageException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentProfileImageException.cs new file mode 100644 index 000000000..4941be0f7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/InvalidStudentProfileImageException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class InvalidStudentProfileImageException : DomainException + { + public override string Code { get; } = "invalid_student_profile_image"; + public Guid Id { get; } + public string ProfileImage { get; } + + public InvalidStudentProfileImageException(Guid id, string profileImage) : base( + $"Student with id: {id} has invalid profile image.") + { + Id = id; + ProfileImage = profileImage; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/StudentStateAlreadySetException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/StudentStateAlreadySetException.cs new file mode 100644 index 000000000..7ea8c18e4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Exceptions/StudentStateAlreadySetException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Exceptions +{ + public class StudentStateAlreadySetException : DomainException + { + public override string Code { get; } = "student_state_already_set"; + public Guid Id { get; } + public State State { get; } + + public StudentStateAlreadySetException(Guid id, State state) : base( + $"Student: {id} has state already set to: {state}.") + { + Id = id; + State = state; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.csproj b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..c0cf01061 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IStudentRepository + { + Task GetAsync(Guid id); + Task AddAsync(Student student); + Task UpdateAsync(Student student); + Task DeleteAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..452ee7ee3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Students.Application; + +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..017039d69 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Students.Application; + +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..d7b3bc70c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..8842f576f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,37 @@ +using MiniSpace.Services.Students.Application; + +namespace MiniSpace.Services.Students.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { 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); + Claims = claims ?? new Dictionary(); + IsOrganizer = Claims["permissions"] == "organize_events"; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..ff4a74a65 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.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.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..124e1ff37 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.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.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..aec8fd29f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,93 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Events.Rejected; +using MiniSpace.Services.Students.Application.Events.External; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Core.Exceptions; + +namespace MiniSpace.Services.Students.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + CannotChangeStudentStateException ex => message switch + { + ChangeStudentState _ => new ChangeStudentStateRejected(ex.Id, + ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + _ => null + }, + CannotUpdateStudentException ex => message switch + { + UpdateStudent command => new UpdateStudentRejected(command.StudentId, ex.Message, ex.Code), + _ => null, + }, + InvalidStudentDateOfBirthException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + InvalidStudentDescriptionException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + UpdateStudent command => new UpdateStudentRejected(command.StudentId, ex.Message, ex.Code), + _ => null, + }, + InvalidStudentFullNameException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + InvalidStudentProfileImageException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + UpdateStudent command => new UpdateStudentRejected(command.StudentId, ex.Message, ex.Code), + _ => null, + }, + InvalidRoleException ex => message switch + { + SignedUp ev => new CreateStudentRejected(ev.UserId, ex.Message, ex.Code), + _ => null, + }, + StudentAlreadyCreatedException ex => message switch + { + SignedUp ev => new CreateStudentRejected(ev.UserId, ex.Message, ex.Code), + _ => null, + }, + StudentAlreadyRegisteredException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + _ => null, + }, + StudentNotFoundException ex => message switch + { + CompleteStudentRegistration _ => new CompleteStudentRegistrationRejected(ex.Id, ex.Message, + ex.Code), + DeleteStudent command => new DeleteStudentRejected(command.StudentId, ex.Message, ex.Code), + UpdateStudent command => new UpdateStudentRejected(command.StudentId, ex.Message, ex.Code), + _ => null, + }, + StudentStateAlreadySetException ex => message switch + { + ChangeStudentState _ => new ChangeStudentStateRejected(ex.Id, + ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), + _ => null + }, + UnauthorizedStudentAccessException ex => message switch + { + DeleteStudent command => new DeleteStudentRejected(command.StudentId, ex.Message, ex.Code), + _ => null, + }, + _ => null + }; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..ca328b2f0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Core.Exceptions; + +namespace MiniSpace.Services.Students.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.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs new file mode 100644 index 000000000..7232e3fd7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs @@ -0,0 +1,140 @@ +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.Students.Application; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Events.External; +using MiniSpace.Services.Students.Application.Events.External.Handlers; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Contexts; +using MiniSpace.Services.Students.Infrastructure.Decorators; +using MiniSpace.Services.Students.Infrastructure.Exceptions; +using MiniSpace.Services.Students.Infrastructure.Logging; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Students.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Students.Infrastructure.Services; + +namespace MiniSpace.Services.Students.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddTransient(); + 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("students") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .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.Students/src/MiniSpace.Services.Students.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..5e1bc3451 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Students.Application; + +namespace MiniSpace.Services.Students.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..2eef6649f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Students.Application.Commands; + +namespace MiniSpace.Services.Students.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UpdateStudent).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..d77c3aeac --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,86 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Events.External; + +namespace MiniSpace.Services.Students.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(UpdateStudent), new HandlerLogTemplate + { + After = "Updated the student with id: {StudentId}." + } + }, + { + typeof(DeleteStudent), new HandlerLogTemplate + { + After = "Deleted the student with id: {StudentId}." + } + }, + { + typeof(CompleteStudentRegistration), new HandlerLogTemplate + { + After = "Completed a registration for the student with id: {StudentId}." + } + }, + { + typeof(ChangeStudentState), new HandlerLogTemplate + { + After = "Changed a student with id: {StudentId} state to: {State}." + } + }, + { + typeof(SignedUp), new HandlerLogTemplate + { + After = "Created a new student with id: {UserId}." + } + }, + { + typeof(StudentShowedInterestInEvent), new HandlerLogTemplate + { + After = "A student with id: {StudentId} has been interested in the event with id: {EventId}." + } + }, + { + typeof(StudentSignedUpToEvent), new HandlerLogTemplate + { + After = "A student with id: {StudentId} has signed up for the event with id: {EventId}." + } + }, + { + typeof(UserBanned), new HandlerLogTemplate + { + After = "A student with id: {UserId} has been banned." + } + }, + { + typeof(UserUnbanned), new HandlerLogTemplate + { + After = "A student with id: {UserId} has been unbanned." + } + }, + { + typeof(OrganizerRightsGranted), new HandlerLogTemplate + { + After = "Organizer rights has been granted for student with id: {UserId}." + } + }, + { + typeof(OrganizerRightsRevoked), new HandlerLogTemplate + { + After = "Organizer rights has been revoked for student with id: {UserId}." + } + } + }; + + 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.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.csproj b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.csproj new file mode 100644 index 000000000..154f2f7f1 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..01e9e0a86 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,55 @@ +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Student AsEntity(this StudentDocument document) + => new Student(document.Id, document.Email, document.CreatedAt, document.FirstName, + document.LastName, document.NumberOfFriends, document.ProfileImage, + document.Description, document.DateOfBirth, document.EmailNotifications, + document.IsBanned, document.CanBeOrganizer, document.State, + document.InterestedInEvents, document.SignedUpEvents); + + public static StudentDocument AsDocument(this Student entity) + => new StudentDocument() + { + Id = entity.Id, + Email = entity.Email, + FirstName = entity.FirstName, + LastName = entity.LastName, + NumberOfFriends = entity.NumberOfFriends, + ProfileImage = entity.ProfileImage, + Description = entity.Description, + DateOfBirth = entity.DateOfBirth, + EmailNotifications = entity.EmailNotifications, + IsBanned = entity.IsBanned, + CanBeOrganizer = entity.CanBeOrganizer, + State = entity.State, + CreatedAt = entity.CreatedAt, + InterestedInEvents = entity.InterestedInEvents, + SignedUpEvents = entity.SignedUpEvents + }; + + public static StudentDto AsDto(this StudentDocument document) + => new StudentDto() + { + Id = document.Id, + Email = document.Email, + FirstName = document.FirstName, + LastName = document.LastName, + NumberOfFriends = document.NumberOfFriends, + ProfileImage = document.ProfileImage, + Description = document.Description, + DateOfBirth = document.DateOfBirth, + EmailNotifications = document.EmailNotifications, + IsBanned = document.IsBanned, + CanBeOrganizer = document.CanBeOrganizer, + State = document.State.ToString().ToLowerInvariant(), + CreatedAt = document.CreatedAt, + InterestedInEvents = document.InterestedInEvents, + SignedUpEvents = document.SignedUpEvents + }; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..4fca3c845 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,24 @@ +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class StudentDocument : IIdentifiable + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool CanBeOrganizer { get; set; } + public State State { get; set; } + public DateTime CreatedAt { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs new file mode 100644 index 000000000..d2e9c2b1a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentEventsHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentEventsHandler : IQueryHandler + { + private readonly IMongoRepository _studentRepository; + + public GetStudentEventsHandler(IMongoRepository repository) + => _studentRepository = repository; + + public async Task HandleAsync(GetStudentEvents query, CancellationToken cancellationToken) + { + var document = await _studentRepository.GetAsync(p => p.Id == query.StudentId); + if(document is null) + { + throw new StudentNotFoundException(query.StudentId); + } + + var studentEvents = new StudentEventsDto() + { + StudentId = document.Id, + InterestedInEvents = document.InterestedInEvents, + SignedUpEvents = document.SignedUpEvents + }; + + return studentEvents; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentHandler.cs new file mode 100644 index 000000000..dd7f04855 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentHandler.cs @@ -0,0 +1,25 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentHandler : IQueryHandler + { + private readonly IMongoRepository _studentRepository; + + public GetStudentHandler(IMongoRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task HandleAsync(GetStudent query, CancellationToken cancellationToken) + { + var document = await _studentRepository.GetAsync(p => p.Id == query.StudentId); + + return document?.AsDto(); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentsHandler.cs new file mode 100644 index 000000000..b90d07fdd --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentsHandler.cs @@ -0,0 +1,25 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentsHandler : IQueryHandler> + { + private readonly IMongoRepository _studentRepository; + + public GetStudentsHandler(IMongoRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task> HandleAsync(GetStudents query, CancellationToken cancellationToken) + { + var students = await _studentRepository.FindAsync(_ => true); + + return students.Select(p => p.AsDto()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 000000000..391ddfb51 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,33 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Students.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(o => o.Id == id); + + return student?.AsEntity(); + } + + public Task AddAsync(Student student) + => _repository.AddAsync(student.AsDocument()); + + public Task UpdateAsync(Student student) + => _repository.UpdateAsync(student.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..f515335bf --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Students.Application.Services; + +namespace MiniSpace.Services.Students.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..808ecaec9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core; +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + case StudentRegistrationCompleted e: return new Application.Events.StudentCreated(e.Student.Id); + case StudentUpdated e: return new Application.Events.StudentUpdated(e.Student.Id); + case StudentStateChanged e: + return new Application.Events.StudentStateChanged(e.Student.Id, + e.Student.State.ToString().ToLowerInvariant(), e.PreviousState.ToString().ToLowerInvariant()); + } + + return null; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..40e4466a2 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.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.Students.Application.Services; + +namespace MiniSpace.Services.Students.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.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs index 9f63183c9..008afbeb6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs @@ -1,12 +1,16 @@ +using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; namespace MiniSpace.Web.Areas.Identity { public interface IIdentityService - { - Task GetAccountAsync(string jwt); - Task SignUpAsync(string email, string password, string role = "user"); + { + public JwtDto JwtDto { get; } + bool IsAuthenticated { get; } + Task GetAccountAsync(); + Task SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", IEnumerable permissions = null); Task SignInAsync(string email, string password); + void Logout(); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs index ddc8fbb32..cfdb7802c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs @@ -17,13 +17,13 @@ public Task OnInit() return Task.CompletedTask; } - public Task SignUpAsync(string email, string password, string role = "user") - => _identityService.SignUpAsync(email, password, role); + public Task SignUpAsync(string firstName, string lastName, string email, string password, string role = "user") + => _identityService.SignUpAsync(firstName, lastName, email, password, role); public Task SignInAsync(string email, string password) => _identityService.SignInAsync(email, password); - public Task GetAccount(string jwt) - => _identityService.GetAccountAsync(jwt); + public Task GetAccount() + => _identityService.GetAccountAsync(); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs index b2686d7ab..0396c3e11 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; using MiniSpace.Web.HttpClients; @@ -7,22 +8,34 @@ namespace MiniSpace.Web.Areas.Identity class IdentityService : IIdentityService { private readonly IHttpClient _httpClient; - + public JwtDto JwtDto { get; private set; } + public bool IsAuthenticated { get; private set; } + public IdentityService(IHttpClient httpClient) { _httpClient = httpClient; } - public Task GetAccountAsync(string jwt) + public Task GetAccountAsync() { - _httpClient.SetAccessToken(jwt); + _httpClient.SetAccessToken(JwtDto.AccessToken); return _httpClient.GetAsync("identity/me"); } - public Task SignUpAsync(string email, string password, string role = "user") - => _httpClient.PostAsync("identity/sign-up", new {email, password, role}); + public Task SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", IEnumerable permissions = null) + => _httpClient.PostAsync("identity/sign-up", new {firstName, lastName, email, password, role, permissions}); - public Task SignInAsync(string email, string password) - => _httpClient.PostAsync("identity/sign-in", new {email, password}); + public async Task SignInAsync(string email, string password) + { + JwtDto = await _httpClient.PostAsync("identity/sign-in", new {email, password}); + IsAuthenticated = true; + return JwtDto; + } + + public void Logout() + { + JwtDto = null; + IsAuthenticated = false; + } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs new file mode 100644 index 000000000..3932d86a5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; + +namespace MiniSpace.Web.Areas.Students +{ + public interface IStudentsService + { + Task GetStudentAsync(Guid studentId); + Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications); + Task CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + string description, DateTime dateOfBirth, bool emailNotifications); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs new file mode 100644 index 000000000..427f5fd5a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Students +{ + public class StudentsService : IStudentsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public StudentsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task GetStudentAsync(Guid studentId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync($"students/{studentId}"); + } + + public Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"students/{studentId}", new {studentId, profileImage, + description, emailNotifications}); + } + + public Task CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + string description, DateTime dateOfBirth, bool emailNotifications) + => _httpClient.PostAsync("students", new {studentId, profileImage, + description, dateOfBirth, emailNotifications}); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs new file mode 100644 index 000000000..35ffe4122 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO +{ + public class StudentDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public int NumberOfFriends { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs index 4de8995a7..dc39d7ef8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs @@ -5,6 +5,7 @@ namespace MiniSpace.Web.DTO public class UserDto { public Guid Id { get; set; } + public string Name { get; set; } public string Email { get; set; } public string Role { get; set; } public DateTime CreatedAt { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecast.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecast.cs deleted file mode 100644 index 368968868..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace MiniSpace.Web.Data -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecastService.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecastService.cs deleted file mode 100644 index bbc82a7f3..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Data/WeatherForecastService.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace MiniSpace.Web.Data -{ - public class WeatherForecastService - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - public Task GetForecastAsync(DateTime startDate) - { - var rng = new Random(); - return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = startDate.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }).ToArray()); - } - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index 6b457aa04..5fe8b26a8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -11,4 +11,9 @@ + + + + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/SignUpModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/SignUpModel.cs index 9a183b378..c8f097953 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/SignUpModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/SignUpModel.cs @@ -7,7 +7,8 @@ namespace MiniSpace.Web.Models.Identity { public class SignUpModel { - public string Username { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } public string Role { get; set; } = "user"; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs new file mode 100644 index 000000000..5938dfe67 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.Models.Students +{ + public class CompleteRegistrationModel + { + public Guid StudentId { get; set; } + public string ProfileImage { get; set; } + public string Description { get; set; } + public DateTime DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor new file mode 100644 index 000000000..05dfc0b92 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor @@ -0,0 +1,65 @@ +@page "/signup/complete" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Models.Students +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager + +

Complete your registration

+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +@code { + private CompleteRegistrationModel completeRegistrationModel = new(); + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + var userDto = await IdentityService.GetAccountAsync(); + completeRegistrationModel.StudentId = userDto.Id; + completeRegistrationModel.ProfileImage = "null"; + completeRegistrationModel.DateOfBirth = DateTime.Now; + } + } + + private async Task HandleCompleteRegistration() + { + await StudentsService.CompleteStudentRegistrationAsync(completeRegistrationModel.StudentId, + completeRegistrationModel.ProfileImage, completeRegistrationModel.Description, + completeRegistrationModel.DateOfBirth, completeRegistrationModel.EmailNotifications); + // Handle the post-sign-up logic, such as redirection or displaying a success message + if (IdentityService.IsAuthenticated) + { + NavigationManager.NavigateTo("/account"); + } + else + { + NavigationManager.NavigateTo("/signin"); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Counter.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Counter.razor deleted file mode 100644 index 59c0d242c..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Counter.razor +++ /dev/null @@ -1,16 +0,0 @@ -@page "/counter" - -

Counter

- -

Current count: @currentCount

- - - -@code { - int currentCount = 0; - - void IncrementCount() - { - currentCount++; - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/FetchData.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/FetchData.razor deleted file mode 100644 index 569feb988..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/FetchData.razor +++ /dev/null @@ -1,46 +0,0 @@ -@page "/fetchdata" - -@using MiniSpace.Web.Data -@inject WeatherForecastService ForecastService - -

Weather forecast

- -

This component demonstrates fetching data from a service.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - WeatherForecast[] forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await ForecastService.GetForecastAsync(DateTime.Now); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor index 84256dfcf..f6185d7e9 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor @@ -1,13 +1,13 @@ @page "/greeting" @using Microsoft.AspNetCore.Components.Authorization -@inject AuthenticationStateProvider AuthenticationStateProvider +@inject IIdentityService IdentityService

Greeting

-@if (userName != null) +@if (email != null) { -

Hello, @userName!

+

Hello, @email!

} else { @@ -15,11 +15,15 @@ else } @code { - private string userName; + private string email; protected override async Task OnInitializedAsync() { - var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - userName = authState.User.Identity.Name; + if (IdentityService.IsAuthenticated) + { + var userDto = await IdentityService.GetAccountAsync(); + email = userDto.Email; + StateHasChanged(); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor index 16dac3192..6db27ff92 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor @@ -1,5 +1,12 @@ @page "/" -

Hello, world!

- -Welcome to your new app. +
+
+
+

Welcome to our Social Media Platform!

+

We're excited to have you join our MiniSpace community.

+

Connect with friends, share moments, and discover new experiences.

+

Start exploring now!

+
+
+
\ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/ShowAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/ShowAccount.razor new file mode 100644 index 000000000..0d3771396 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/ShowAccount.razor @@ -0,0 +1,126 @@ +@page "/account" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager + +

Data of your account:

+ +@if (studentDto.Id != Guid.Empty) +{ +
+
+
+ Firstname: +
+
+ @studentDto.FirstName +
+
+ +
+
+ Lastname: +
+
+ @studentDto.LastName +
+
+ +
+
+ Email: +
+
+ @studentDto.Email +
+
+ +
+
+ Number of friends: +
+
+ @studentDto.NumberOfFriends +
+
+ +
+
+ Description: +
+
+ @studentDto.Description +
+
+ +
+
+ Date of birth: +
+
+ @studentDto.DateOfBirth?.ToString("dd-MM-yyyy") +
+
+ +
+
+ Email notifications: +
+
+ @studentDto.EmailNotifications +
+
+ +
+
+ State: +
+
+ @studentDto.State +
+
+ +
+
+ Created at: +
+
+ @studentDto.CreatedAt.ToString("dd-MM-yyyy") +
+
+
+ +
 
+ + @if (studentDto.State == "incomplete") + { + + } + else + { + + } +} +else +{ +

Loading...

+} + +@code { + private StudentDto studentDto = new(); + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + var userDto = await IdentityService.GetAccountAsync(); + studentDto = await StudentsService.GetStudentAsync(userDto.Id); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor index e27c6a3d2..b52f46e60 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor @@ -1,10 +1,11 @@ @page "/signin" @using MiniSpace.Web.Models.Identity @using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Students @inject IIdentityService IdentityService +@inject IStudentsService StudentsService @inject NavigationManager NavigationManager -

Login

@if (showError) @@ -40,7 +41,10 @@ var jwtDto = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); if (jwtDto != null && !string.IsNullOrEmpty(jwtDto.AccessToken)) { - NavigationManager.NavigateTo("/greeting"); + var userDto = await IdentityService.GetAccountAsync(); + var studentDto = await StudentsService.GetStudentAsync(userDto.Id); + + NavigationManager.NavigateTo(studentDto.State == "incomplete" ? "/signup/complete" : "/account"); } else { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor index d5a64e78d..e9462da48 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor @@ -1,21 +1,32 @@ @page "/signup" @using MiniSpace.Web.Areas.Identity @inject IIdentityService IdentityService +@inject NavigationManager NavigationManager

Sign Up

-@* + @*
*@ -
- - +
+
+
+ + +
+
+
+
+ + +
+
@@ -37,7 +48,16 @@ private async Task HandleSignUp() { - await IdentityService.SignUpAsync(signUpModel.Email, signUpModel.Password, "user"); - // Handle the post-sign-up logic, such as redirection or displaying a success message + var organizerPermission = new List { "organize_events" }; + try + { + await IdentityService.SignUpAsync(signUpModel.FirstName, signUpModel.LastName, signUpModel.Email, + signUpModel.Password, "user", organizerPermission); + NavigationManager.NavigateTo("/signin"); + } + catch (Exception ex) + { + + } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor new file mode 100644 index 000000000..51c758c0e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor @@ -0,0 +1,60 @@ +@page "/account/update" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager + +

Update some data in your account:

+ +@if (showError) +{ +
+ Failed to sign in. Please check your credentials and try again. +
+} + + + + + + @if (studentDto.Id != Guid.Empty) + { +
+ + +
+ +
+ + +
+ + + } + else + { +

Loading...

+ } +
+ +@code { + private StudentDto studentDto = new(); + private bool showError = false; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + var userDto = await IdentityService.GetAccountAsync(); + studentDto = await StudentsService.GetStudentAsync(userDto.Id); + } + } + + private async Task HandleUpdateStudent() + { + await StudentsService.UpdateStudentAsync(studentDto.Id, studentDto.ProfileImage, + studentDto.Description, studentDto.EmailNotifications); + NavigationManager.NavigateTo("/account"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor index 52be47d74..eeb9ba241 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor @@ -1,6 +1,6 @@ -@* @inject HttpClient Http *@ -@using Microsoft.AspNetCore.Components.Authorization -@inject AuthenticationStateProvider AuthenticationStateProvider +@* @inject HttpClient Http *@ +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager @@ -56,8 +55,8 @@ protected override async Task OnInitializedAsync() { - var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - isUserAuthenticated = authState.User.Identity.IsAuthenticated; + NavigationManager.LocationChanged += LocationChanged; + isUserAuthenticated = IdentityService.IsAuthenticated; } string NavMenuCssClass => collapseNavMenu ? "collapse" : null; @@ -65,8 +64,16 @@ { collapseNavMenu = !collapseNavMenu; } + + void LocationChanged(object sender, LocationChangedEventArgs e) + { + InvokeAsync(StateHasChanged); + } + void SignOut() { // Implement your sign-out logic here, e.g., redirect to sign-out path, clear authentication state + IdentityService.Logout(); + NavigationManager.NavigateTo(""); } -} +} \ 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 5ab1ad622..cab27eac8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -9,9 +9,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MiniSpace.Web.Data; +//using MiniSpace.Web.Data; using MiniSpace.Web.Models.Identity; using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Areas.Students; using MiniSpace.Web.HttpClients; @@ -32,7 +33,6 @@ public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); - services.AddSingleton(); var httpClientOptions = Configuration.GetSection("HttpClientOptions").Get(); @@ -49,6 +49,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/MiniSpace.Web/src/MiniSpace.Web/appsettings.Development.json b/MiniSpace.Web/src/MiniSpace.Web/appsettings.Development.json index e203e9407..00f039a34 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/appsettings.Development.json +++ b/MiniSpace.Web/src/MiniSpace.Web/appsettings.Development.json @@ -1,9 +1,20 @@ { "Logging": { "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "HttpClientOptions": { + "ApiUrl": "http://localhost:5000", + "Retries": 3 + }, + "Services": { + "IdentityService": { + "BaseUrl": "http://localhost:5004" } } + } diff --git a/MiniSpace/compose/infrastructure.yml b/MiniSpace/compose/infrastructure.yml index 7a6f23df0..bb4fff88f 100644 --- a/MiniSpace/compose/infrastructure.yml +++ b/MiniSpace/compose/infrastructure.yml @@ -82,9 +82,12 @@ services: networks: - minispace ports: - - 5672:5672 - - 15672:15672 - - 15692:15692 + - 5672:5672 # default AMQP port + - 15672:15672 # default management UI port + - 15692:15692 # already existing custom port + - 25672:25672 # clustering port used for inter-node and CLI tools communication + - 5671:5671 # secure AMQP port (TLS) + - 15671:15671 # secure management UI port (TLS) # volumes: # - rabbitmq:/var/lib/rabbitmq diff --git a/MiniSpace/compose/services.yml b/MiniSpace/compose/services.yml index 072df36f7..a4b7dfd6a 100644 --- a/MiniSpace/compose/services.yml +++ b/MiniSpace/compose/services.yml @@ -20,6 +20,24 @@ services: - 5004:80 networks: - minispace + + students-service: + image: adrianvsaint/minispace.services.students:latest + container_name: students-service + restart: unless-stopped + ports: + - 5007:80 + networks: + - minispace + + events-service: + image: adrianvsaint/minispace.services.events:latest + container_name: events-service + restart: unless-stopped + ports: + - 5008:80 + networks: + - minispace web: image: adrianvsaint/minispace.web:latest diff --git a/MiniSpace/scripts/dockerize-all.sh b/MiniSpace/scripts/dockerize-all.sh index 5cdce6238..586c9d274 100755 --- a/MiniSpace/scripts/dockerize-all.sh +++ b/MiniSpace/scripts/dockerize-all.sh @@ -4,6 +4,8 @@ directories=( "MiniSpace.APIGateway" "MiniSpace.Web" "MiniSpace.Services.Identity" + "MiniSpace.Services.Events" + "MiniSpace.Services.Students" ) for dir in "${directories[@]}"; do