diff --git a/.github/workflows/azure_synchronisation.yml b/.github/workflows/azure_synchronisation.yml index e49e4980f..b296c030c 100644 --- a/.github/workflows/azure_synchronisation.yml +++ b/.github/workflows/azure_synchronisation.yml @@ -29,7 +29,13 @@ jobs: 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_PAT@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO + # Check if the remote already exists + if git remote get-url azure; then + echo "Remote 'azure' already exists. Updating URL if needed." + git remote set-url azure https://$AZURE_DEVOPS_PAT@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO + else + git remote add azure https://$AZURE_DEVOPS_PAT@dev.azure.com/SocialAppOIPproject/SocialApp_IO/_git/SocialApp_IO + fi - name: Push all branches to Azure DevOps run: git push azure --all --force diff --git a/.github/workflows/build_microservices.yml b/.github/workflows/build_microservices.yml index d08bea385..2d60a9355 100644 --- a/.github/workflows/build_microservices.yml +++ b/.github/workflows/build_microservices.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build-and-test: - runs-on: ubuntu-latest + runs-on: ubuntu-latest timeout-minutes: 30 strategy: matrix: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml index 03b68c692..d5102ddea 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml @@ -97,7 +97,7 @@ modules: returnValue: Welcome to MiniSpace API [async]! identity: - path: identity + path: /identity routes: - upstream: /users/{userId} method: GET @@ -112,7 +112,7 @@ modules: use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -124,13 +124,516 @@ modules: - upstream: /sign-in method: POST - auth: false use: downstream downstream: identity-service/sign-in - responseHeaders: - content-type: application/json + auth: false + + - upstream: /users/{userId}/organizer-rights + method: POST + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/organizer-rights + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/ban + method: POST + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true + + - upstream: /users/{userId}/ban + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true services: identity-service: localUrl: localhost:5004 - url: identity-service \ No newline at end of file + url: identity-service + + 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 + + - upstream: /{studentId} + method: GET + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: /{studentId} + method: PUT + use: downstream + downstream: students-service/students/{studentId} + bind: + - studentId:{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} + bind: + - studentId:{studentId} + - state:{state} + auth: true + claims: + role: admin + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + 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: /student/{studentId} + method: GET + use: downstream + downstream: events-service/events/student/{studentId} + auth: true + + - 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: /search/organizer + method: POST + use: downstream + downstream: events-service/events/search/organizer + auth: true + + - upstream: /{eventId}/show-interest + method: POST + use: downstream + downstream: events-service/events/{eventId}/show-interest + auth: true + + - upstream: /{eventId}/show-interest + method: DELETE + 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: /{eventId}/sign-up + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/sign-up + auth: true + + - upstream: /{eventId}/rate + method: POST + use: downstream + downstream: events-service/events/{eventId}/rate + 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} + bind: + - commentId:{commentId} + 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 + bind: + - commentId:{commentId} + auth: true + + - upstream: /{commentId}/like + method: DELETE + use: downstream + downstream: comments-service/comments/{commentId}/like + bind: + - commentId:{commentId} + auth: true + + - upstream: /search + method: POST + use: downstream + downstream: comments-service/comments/search + + 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: /{reactionId} + method: DELETE + use: downstream + downstream: reactions-service/reactions/{reactionId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: reactions-service/reactions + + - upstream: /summary + method: GET + use: downstream + downstream: reactions-service/reactions/summary + + 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: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true + + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true + + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true + + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + 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} + bind: + - postId:{postId} + auth: true + + - upstream: /{postId}/state/{state} + method: PUT + use: downstream + downstream: posts-service/posts/{postId}/state/{state} + bind: + - postId:{postId} + - state:{state} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: posts-service/posts + + - upstream: /{postId} + method: DELETE + use: downstream + downstream: posts-service/posts/{postId} + auth: true + + + services: + posts-service: + localUrl: localhost:5013 + url: posts-service + + organizations: + path: /organizations + routes: + - upstream: / + method: POST + use: downstream + downstream: organizations-service/organizations + auth: true + + - upstream: /organizer/{organizationId}/organizer + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer + auth: true + + - upstream: /organizer/{organizationId}/organizer/{organizerId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: organizations-service/organizations + + - upstream: /{organizationId} + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId} + + - upstream: /{organizationId}/details + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/details + + - upstream: /root + method: GET + use: downstream + downstream: organizations-service/organizations/root + + - upstream: /{organizationId}/children + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + + - upstream: /organizer/{organizerId} + method: GET + use: downstream + downstream: organizations-service/organizations/organizer/{organizerId} + auth: true + + services: + organizations-service: + localUrl: localhost:5015 + url: organizations-service + + diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml index 3bc4ebb7b..8475d0118 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml @@ -95,9 +95,8 @@ modules: method: GET use: return_value returnValue: Welcome to MiniSpace API [async]! - identity: - path: identity + path: /identity routes: - upstream: /users/{userId} method: GET @@ -112,7 +111,7 @@ modules: use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -124,12 +123,536 @@ modules: - upstream: /sign-in method: POST - auth: false use: downstream downstream: identity-service/sign-in - + auth: false + + - upstream: /users/{userId}/organizer-rights + method: POST + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/organizer-rights + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/ban + method: POST + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true + + - upstream: /users/{userId}/ban + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true services: identity-service: localUrl: localhost:5004 url: identity-service + + + + 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 + + - upstream: /{studentId} + method: GET + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: /{studentId} + method: PUT + use: downstream + downstream: students-service/students/{studentId} + bind: + - studentId:{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} + bind: + - studentId:{studentId} + - state:{state} + auth: true + claims: + role: admin + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + 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: /student/{studentId} + method: GET + use: downstream + downstream: events-service/events/student/{studentId} + auth: true + + - 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: /search/organizer + method: POST + use: downstream + downstream: events-service/events/search/organizer + auth: true + + - upstream: /{eventId}/show-interest + method: POST + use: downstream + downstream: events-service/events/{eventId}/show-interest + auth: true + + - upstream: /{eventId}/show-interest + method: DELETE + 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: /{eventId}/sign-up + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/sign-up + auth: true + + - upstream: /{eventId}/rate + method: POST + use: downstream + downstream: events-service/events/{eventId}/rate + 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} + bind: + - commentId:{commentId} + 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 + bind: + - commentId:{commentId} + auth: true + + - upstream: /{commentId}/like + method: DELETE + use: downstream + downstream: comments-service/comments/{commentId}/like + bind: + - commentId:{commentId} + auth: true + + - upstream: /search + method: POST + use: downstream + downstream: comments-service/comments/search + + 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: /{reactionId} + method: DELETE + use: downstream + downstream: reactions-service/reactions/{reactionId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: reactions-service/reactions + + - upstream: /summary + method: GET + use: downstream + downstream: reactions-service/reactions/summary + + 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: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true + + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true + + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true + + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + 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} + bind: + - postId:{postId} + auth: true + + - upstream: /{postId}/state/{state} + method: PUT + use: downstream + downstream: posts-service/posts/{postId}/state/{state} + bind: + - postId:{postId} + - state:{state} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: posts-service/posts + + - upstream: /{postId} + method: DELETE + use: downstream + downstream: posts-service/posts/{postId} + auth: true + + + services: + posts-service: + localUrl: localhost:5013 + url: posts-service + + + + organizations: + path: /organizations + routes: + - upstream: / + method: POST + use: downstream + downstream: organizations-service/organizations + auth: true + + - upstream: /organizer/{organizationId}/organizer + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer + auth: true + + - upstream: /organizer/{organizationId}/organizer/{organizerId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: organizations-service/organizations + + - upstream: /{organizationId} + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId} + + - upstream: /{organizationId}/details + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/details + + - upstream: /root + method: GET + use: downstream + downstream: organizations-service/organizations/root + + - upstream: /{organizationId}/children + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + + - upstream: /organizer/{organizerId} + method: GET + use: downstream + downstream: organizations-service/organizations/organizer/{organizerId} + auth: true + + services: + organizations-service: + localUrl: localhost:5015 + url: organizations-service + + diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml index c77d6aca1..221305f1d 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml @@ -29,9 +29,11 @@ extensions: allowedOrigins: - '*' allowedMethods: - - post - - put - - delete + - GET + - POST + - PUT + - DELETE + - OPTIONS allowedHeaders: - '*' exposedHeaders: @@ -42,10 +44,10 @@ extensions: jwt: issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij - validIssuer: pacco + validIssuer: minispace validateAudience: false - validateIssuer: true - validateLifetime: true + validateIssuer: false + validateLifetime: false swagger: name: MiniSpace @@ -72,7 +74,7 @@ modules: returnValue: Welcome to MiniSpace API! identity: - path: identity + path: /identity routes: - upstream: /users/{userId} method: GET @@ -102,10 +104,518 @@ modules: use: downstream downstream: identity-service/sign-in auth: false - responseHeaders: - content-type: application/json - + + - upstream: /users/{userId}/organizer-rights + method: POST + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/organizer-rights + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/organizer-rights + auth: true + + - upstream: /users/{userId}/ban + method: POST + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true + + - upstream: /users/{userId}/ban + method: DELETE + use: downstream + downstream: identity-service/users/{userId}/ban + auth: true + services: identity-service: localUrl: localhost:5004 url: identity-service + + 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 + + - upstream: /{studentId} + method: GET + use: downstream + downstream: students-service/students/{studentId} + auth: true + + - upstream: /{studentId} + method: PUT + use: downstream + downstream: students-service/students/{studentId} + bind: + - studentId:{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} + bind: + - studentId:{studentId} + - state:{state} + auth: true + claims: + role: admin + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + 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: /student/{studentId} + method: GET + use: downstream + downstream: events-service/events/student/{studentId} + auth: true + + - 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: /search/organizer + method: POST + use: downstream + downstream: events-service/events/search/organizer + auth: true + + - upstream: /{eventId}/show-interest + method: POST + use: downstream + downstream: events-service/events/{eventId}/show-interest + auth: true + + - upstream: /{eventId}/show-interest + method: DELETE + 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: /{eventId}/sign-up + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/sign-up + auth: true + + - upstream: /{eventId}/rate + method: POST + use: downstream + downstream: events-service/events/{eventId}/rate + 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} + bind: + - commentId:{commentId} + 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 + bind: + - commentId:{commentId} + auth: true + + - upstream: /{commentId}/like + method: DELETE + use: downstream + downstream: comments-service/comments/{commentId}/like + bind: + - commentId:{commentId} + auth: true + + - upstream: /search + method: POST + use: downstream + downstream: comments-service/comments/search + + 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: /{reactionId} + method: DELETE + use: downstream + downstream: reactions-service/reactions/{reactionId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: reactions-service/reactions + + - upstream: /summary + method: GET + use: downstream + downstream: reactions-service/reactions/summary + + 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: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true + + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true + + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true + + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true + + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true + + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + 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} + bind: + - postId:{postId} + auth: true + + - upstream: /{postId}/state/{state} + method: PUT + use: downstream + downstream: posts-service/posts/{postId}/state/{state} + bind: + - postId:{postId} + - state:{state} + auth: true + + - upstream: /{postId} + method: GET + use: downstream + downstream: posts-service/posts/{postId} + + - upstream: / + method: GET + use: downstream + downstream: posts-service/posts + + - upstream: /{postId} + method: DELETE + use: downstream + downstream: posts-service/posts/{postId} + auth: true + + + services: + posts-service: + localUrl: localhost:5013 + url: posts-service + + organizations: + path: /organizations + routes: + - upstream: / + method: POST + use: downstream + downstream: organizations-service/organizations + auth: true + + - upstream: /organizer/{organizationId}/organizer + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer + auth: true + + - upstream: /organizer/{organizationId}/organizer/{organizerId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: organizations-service/organizations + + - upstream: /{organizationId} + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId} + + - upstream: /{organizationId}/details + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/details + + - upstream: /root + method: GET + use: downstream + downstream: organizations-service/organizations/root + + - upstream: /{organizationId}/children + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + + - upstream: /organizer/{organizerId} + method: GET + use: downstream + downstream: organizations-service/organizations/organizer/{organizerId} + auth: true + + services: + organizations-service: + localUrl: localhost:5015 + url: organizations-service + + diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 1a9e8a03b..492edb433 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -18,7 +18,7 @@ generateTraceId: true useLocalUrl: true loadBalancer: enabled: false - url: faibo:9999 + url: fabio:9999 extensions: customErrors: @@ -112,32 +112,24 @@ modules: use: downstream downstream: identity-service/users/{userId}/organizer-rights auth: true - claims: - role: admin - upstream: /users/{userId}/organizer-rights method: DELETE use: downstream downstream: identity-service/users/{userId}/organizer-rights auth: true - claims: - role: admin - upstream: /users/{userId}/ban method: POST use: downstream downstream: identity-service/users/{userId}/ban auth: true - claims: - role: admin - upstream: /users/{userId}/ban method: DELETE use: downstream downstream: identity-service/users/{userId}/ban auth: true - claims: - role: admin services: identity-service: @@ -210,8 +202,6 @@ modules: use: downstream downstream: students-service/students auth: true - claims: - role: admin - upstream: /{studentId} method: GET @@ -299,6 +289,12 @@ modules: method: POST use: downstream downstream: events-service/events/search + + - upstream: /search/organizer + method: POST + use: downstream + downstream: events-service/events/search/organizer + auth: true - upstream: /{eventId}/show-interest method: POST @@ -306,12 +302,24 @@ modules: downstream: events-service/events/{eventId}/show-interest auth: true + - upstream: /{eventId}/show-interest + method: DELETE + 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: /{eventId}/sign-up + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/sign-up + auth: true + - upstream: /{eventId}/rate method: POST use: downstream @@ -324,6 +332,24 @@ modules: downstream: events-service/events/organizer/{organizerId} auth: true + - upstream: /{eventId}/participants + method: GET + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: POST + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + + - upstream: /{eventId}/participants + method: DELETE + use: downstream + downstream: events-service/events/{eventId}/participants + auth: true + services: events-service: localUrl: localhost:5008 @@ -348,12 +374,6 @@ modules: - commentId:{commentId} auth: true - - upstream: / - method: GET - use: downstream - downstream: comments-service/comments - auth: true - - upstream: /{commentId} method: DELETE use: downstream @@ -368,6 +388,19 @@ modules: - commentId:{commentId} auth: true + - upstream: /{commentId}/like + method: DELETE + use: downstream + downstream: comments-service/comments/{commentId}/like + bind: + - commentId:{commentId} + auth: true + + - upstream: /search + method: POST + use: downstream + downstream: comments-service/comments/search + services: comments-service: localUrl: localhost:5009 @@ -384,17 +417,21 @@ modules: downstream: reactions-service/reactions auth: true - - upstream: / + - upstream: /{reactionId} method: DELETE use: downstream - downstream: reactions-service/reactions + downstream: reactions-service/reactions/{reactionId} auth: true - upstream: / method: GET use: downstream downstream: reactions-service/reactions - auth: true + + - upstream: /summary + method: GET + use: downstream + downstream: reactions-service/reactions/summary services: reactions-service: @@ -428,56 +465,89 @@ modules: statistics-service: localUrl: localhost:5011 url: statistics-service - + friends: - path: /friends - routes: - - upstream: / - method: GET - use: downstream - downstream: friends-service/friends - auth: true + 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} - bind: - - userId:{userId} - auth: true + - upstream: /{userId} + method: GET + use: downstream + downstream: friends-service/friends/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /notYet - method: GET - use: downstream - downstream: friends-service/friends/notYet - auth: true + - upstream: /{userId}/invite + method: POST + use: downstream + downstream: friends-service/friends/{userId}/invite + bind: + - userId: {userId} + auth: true - - upstream: /pending - method: POST - use: downstream - downstream: friends-service/friends/pending - auth: true + - upstream: /{requesterId}/{friendId}/remove + method: DELETE + use: downstream + downstream: friends-service/friends/{requesterId}/{friendId}/remove + bind: + - friendId: {friendId} + - requesterId: {requesterId} + auth: true - - upstream: /pending - method: GET - use: downstream - downstream: friends-service/friends/pending - auth: true + - upstream: /requests/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/{userId} + bind: + - userId: {userId} + auth: true - - upstream: /{userId}/invite - method: POST - use: downstream - downstream: friends-service/friends/{userId}/invite - bind: - - userId:{userId} - auth: true + - upstream: /requests/{userId}/accept + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/accept + bind: + - userId: {userId} + auth: true - services: - friends-service: - localUrl: localhost:5012 - url: friends-service + - upstream: /requests/{userId}/decline + method: POST + use: downstream + downstream: friends-service/friends/requests/{userId}/decline + bind: + - userId: {userId} + auth: true + + - upstream: /pending + method: GET + use: downstream + downstream: friends-service/friends/pending + auth: true + + - upstream: /pending/all + method: GET + use: downstream + downstream: friends-service/friends/pending/all + auth: true + + - upstream: /requests/sent/{userId} + method: GET + use: downstream + downstream: friends-service/friends/requests/sent/{userId} + auth: true + + services: + friends-service: + localUrl: localhost:5012 + url: friends-service @@ -501,12 +571,17 @@ modules: - upstream: /{postId}/state/{state} method: PUT use: downstream - downstream: students-service/students/{postId}/state/{state} + downstream: posts-service/posts/{postId}/state/{state} bind: - postId:{postId} - state:{state} auth: true + - upstream: /{postId} + method: GET + use: downstream + downstream: posts-service/posts/{postId} + - upstream: / method: GET use: downstream @@ -522,4 +597,65 @@ modules: services: posts-service: localUrl: localhost:5013 - url: posts-service \ No newline at end of file + url: posts-service + + + + organizations: + path: /organizations + routes: + - upstream: / + method: POST + use: downstream + downstream: organizations-service/organizations + auth: true + + - upstream: /organizer/{organizationId}/organizer + method: POST + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer + auth: true + + - upstream: /organizer/{organizationId}/organizer/{organizerId} + method: DELETE + use: downstream + downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} + auth: true + + - upstream: / + method: GET + use: downstream + downstream: organizations-service/organizations + + - upstream: /{organizationId} + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId} + + - upstream: /{organizationId}/details + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/details + + - upstream: /root + method: GET + use: downstream + downstream: organizations-service/organizations/root + + - upstream: /{organizationId}/children + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children + + - upstream: /organizer/{organizerId} + method: GET + use: downstream + downstream: organizations-service/organizations/organizer/{organizerId} + auth: true + + services: + organizations-service: + localUrl: localhost:5015 + url: organizations-service + + diff --git a/MiniSpace.Services.Comments/.gitignore b/MiniSpace.Services.Comments/.gitignore new file mode 100644 index 000000000..104b54414 --- /dev/null +++ b/MiniSpace.Services.Comments/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# 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 +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.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 + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# 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 +# NuGet Symbol Packages +*.snupkg +# 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 +*.appxbundle +*.appxupload + +# 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 +*.pfx +*.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 +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# 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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# CodeRush personal settings +.cr/personal + +# 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/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/MiniSpace.Services.Comments/Dockerfile b/MiniSpace.Services.Comments/Dockerfile new file mode 100644 index 000000000..357241e47 --- /dev/null +++ b/MiniSpace.Services.Comments/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Comments.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.Comments.Api.dll"] diff --git a/MiniSpace.Services.Comments/LICENSE b/MiniSpace.Services.Comments/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.Comments/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.Comments/MiniSpace.Services.Comments.sln b/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln new file mode 100644 index 000000000..b474e20c8 --- /dev/null +++ b/MiniSpace.Services.Comments/MiniSpace.Services.Comments.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Api", "src\MiniSpace.Services.Comments.Api\MiniSpace.Services.Comments.Api.csproj", "{F0C47524-349D-48B6-9866-9072D9294FF7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Application", "src\MiniSpace.Services.Comments.Application\MiniSpace.Services.Comments.Application.csproj", "{AEA4B1F6-080B-4BAD-94FA-F7489A2D29B7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Core", "src\MiniSpace.Services.Comments.Core\MiniSpace.Services.Comments.Core.csproj", "{2C8AC5D4-011F-4545-9C0C-88D99D13FF82}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Infrastructure", "src\MiniSpace.Services.Comments.Infrastructure\MiniSpace.Services.Comments.Infrastructure.csproj", "{85941F19-E28E-467B-A338-7D358D32CFC8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F0C47524-349D-48B6-9866-9072D9294FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0C47524-349D-48B6-9866-9072D9294FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0C47524-349D-48B6-9866-9072D9294FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0C47524-349D-48B6-9866-9072D9294FF7}.Release|Any CPU.Build.0 = Release|Any CPU + {AEA4B1F6-080B-4BAD-94FA-F7489A2D29B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEA4B1F6-080B-4BAD-94FA-F7489A2D29B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEA4B1F6-080B-4BAD-94FA-F7489A2D29B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEA4B1F6-080B-4BAD-94FA-F7489A2D29B7}.Release|Any CPU.Build.0 = Release|Any CPU + {2C8AC5D4-011F-4545-9C0C-88D99D13FF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C8AC5D4-011F-4545-9C0C-88D99D13FF82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C8AC5D4-011F-4545-9C0C-88D99D13FF82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C8AC5D4-011F-4545-9C0C-88D99D13FF82}.Release|Any CPU.Build.0 = Release|Any CPU + {85941F19-E28E-467B-A338-7D358D32CFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85941F19-E28E-467B-A338-7D358D32CFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85941F19-E28E-467B-A338-7D358D32CFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85941F19-E28E-467B-A338-7D358D32CFC8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {30DA4571-97CC-4095-8214-8558CF1F0539} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Comments/scripts/build.sh b/MiniSpace.Services.Comments/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Comments/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Comments/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..e10e9e528 --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.comments:latest . + +docker tag minispace.services.comments:latest adrianvsaint/minispace.services.comments:latest + +docker push adrianvsaint/minispace.services.comments:latest diff --git a/MiniSpace.Services.Comments/scripts/dockerize.sh b/MiniSpace.Services.Comments/scripts/dockerize.sh new file mode 100644 index 000000000..d3dfa7b19 --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/dockerize.sh @@ -0,0 +1,21 @@ +#!/bin/bash +TAG='' +VERSION_TAG= + +case "$TRAVIS_BRANCH" in + "master") + TAG=latest + VERSION_TAG=$TRAVIS_BUILD_NUMBER + ;; + "develop") + TAG=dev + VERSION_TAG=$TAG-$TRAVIS_BUILD_NUMBER + ;; +esac + +REPOSITORY=$DOCKER_USERNAME/minispace.services.comments + +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD +docker build -t $REPOSITORY:$TAG -t $REPOSITORY:$VERSION_TAG . +docker push $REPOSITORY:$TAG +docker push $REPOSITORY:$VERSION_TAG \ No newline at end of file diff --git a/MiniSpace.Services.Comments/scripts/start-async.sh b/MiniSpace.Services.Comments/scripts/start-async.sh new file mode 100755 index 000000000..0b61fec74 --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/start-async.sh @@ -0,0 +1,5 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +export NTRADA_CONFIG=ntrada-async +cd src/MiniSpace.Services.Comments.Api +dotnet run diff --git a/MiniSpace.Services.Comments/scripts/start.sh b/MiniSpace.Services.Comments/scripts/start.sh new file mode 100755 index 000000000..2df375d6f --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd src/MiniSpace.APIGateway +dotnet run diff --git a/MiniSpace.Services.Comments/scripts/test.sh b/MiniSpace.Services.Comments/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Comments/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.csproj b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.csproj new file mode 100644 index 000000000..c66b12422 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + latest + MiniSpace.Services.Identity.Api + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs new file mode 100644 index 000000000..27a5df313 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs @@ -0,0 +1,52 @@ +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.Comments.Application; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.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("comments/search", async (cmd, ctx) => + { + var pagedResult = await ctx.RequestServices.GetService().BrowseCommentsAsync(cmd); + await ctx.Response.WriteJsonAsync(pagedResult); + })) + .UseDispatcherEndpoints(endpoints => endpoints + .Post("comments") + .Put("comments/{commentID}") + .Delete("comments/{commentID}") + .Post("comments/{commentID}/like") + .Delete("comments/{commentID}/like") + ) + ) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Properties/launchSettings.json b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Properties/launchSettings.json new file mode 100644 index 000000000..5c713c378 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5009" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Comments": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5009", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.Development.json b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.Development.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.docker.json b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.docker.json new file mode 100644 index 000000000..e1d06000d --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Comments Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "comments-service", + "address": "comments-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "comments-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": "comments", + "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": "comments-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "comments-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": "comments" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "comments-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.Comments/src/MiniSpace.Services.Comments.Api/appsettings.json b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.local.json b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.local.json new file mode 100644 index 000000000..e0c12ec2f --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/appsettings.local.json @@ -0,0 +1,196 @@ +{ + "app": { + "name": "MiniSpace Comments Service", + "service": "comments-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "comments-service", + "address": "docker.for.win.localhost", + "port": "5009", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "comments-service" + }, + "httpClient": { + "type": "direct", + "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": "comments", + "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": "comments-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "comments-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": "comments" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "comments-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "comments:" + }, + "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": "comments-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "comments-service", + "commonName": "comments-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "comments-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs new file mode 100644 index 000000000..440c96dc4 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs @@ -0,0 +1,15 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class AddLike : ICommand + { + public Guid CommentId { get; set; } + + public AddLike(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/CreateComment.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/CreateComment.cs new file mode 100644 index 000000000..54519972e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/CreateComment.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class CreateComment : ICommand + { + public Guid CommentId { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid StudentId { get; set; } + public Guid ParentId { get; set; } + public string Comment { get; set; } + + + public CreateComment(Guid commentId, Guid contextId, string commentContext, Guid studentId, Guid parentId, + string comment) + { + CommentId = commentId == Guid.Empty ? Guid.NewGuid() : commentId; + ContextId = contextId; + CommentContext = commentContext; + StudentId = studentId; + ParentId = parentId; + Comment = comment; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteComment.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteComment.cs new file mode 100644 index 000000000..8ff86829c --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteComment.cs @@ -0,0 +1,12 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class DeleteComment : ICommand + { + public Guid CommentId { get; } + + public DeleteComment(Guid commentId) => CommentId = commentId; + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteLike.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteLike.cs new file mode 100644 index 000000000..7d3061195 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/DeleteLike.cs @@ -0,0 +1,12 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class DeleteLike : ICommand + { + public Guid CommentId { get; } + + public DeleteLike(Guid commentId) => CommentId = commentId; + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs new file mode 100644 index 000000000..abd04f183 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Commands.Handlers +{ + public class AddLikeHandler : ICommandHandler + { + private readonly ICommentRepository _commentRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public AddLikeHandler(ICommentRepository commentRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _commentRepository = commentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AddLike command, CancellationToken cancellationToken = default) + { + var comment = await _commentRepository.GetAsync(command.CommentId); + if (comment is null) + { + throw new CommentNotFoundException(command.CommentId); + } + + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); + } + + comment.Like(identity.Id); + await _commentRepository.UpdateAsync(comment); + + await _messageBroker.PublishAsync(new CommentUpdated(command.CommentId)); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs new file mode 100644 index 000000000..ae067c0cb --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Exceptions; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Commands.Handlers +{ + public class CreateCommentHandler : ICommandHandler + { + private readonly ICommentRepository _commentRepository; + private readonly IStudentRepository _studentRepository; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public CreateCommentHandler(ICommentRepository commentRepository, IStudentRepository studentRepository, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker, IAppContext appContext) + { + _commentRepository = commentRepository; + _studentRepository = studentRepository; + _dateTimeProvider = dateTimeProvider; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(CreateComment command, CancellationToken cancellationToken = default) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != command.StudentId) + { + throw new UnauthorizedCommentAccessException(command.ContextId, identity.Id); + } + if (!(await _studentRepository.ExistsAsync(command.StudentId))) + { + throw new StudentNotFoundException(command.StudentId); + } + + if (!Enum.TryParse(command.CommentContext, true, out var newCommentContext)) + { + throw new InvalidCommentContextEnumException(command.CommentContext); + } + + var now = _dateTimeProvider.Now; + var comment = Comment.Create(command.CommentId, command.ContextId, newCommentContext, command.StudentId, + identity.Name, command.ParentId, command.Comment, now); + + if (command.ParentId != Guid.Empty) + { + var parentComment = await _commentRepository.GetAsync(command.ParentId); + if (parentComment is null) + { + throw new ParentCommentNotFoundException(command.ParentId); + } + if (parentComment.ParentId != Guid.Empty) + { + throw new InvalidParentCommentException(command.ParentId); + } + parentComment.AddReply(now); + await _commentRepository.UpdateAsync(parentComment); + } + + await _commentRepository.AddAsync(comment); + await _messageBroker.PublishAsync(new CommentCreated(command.CommentId)); + } + + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteCommentHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteCommentHandler.cs new file mode 100644 index 000000000..447d23985 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteCommentHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Commands.Handlers +{ + public class DeleteCommentHandler : ICommandHandler + { + private readonly ICommentRepository _commentRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteCommentHandler(ICommentRepository commentRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _commentRepository = commentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteComment command, CancellationToken cancellationToken = default) + { + var comment = await _commentRepository.GetAsync(command.CommentId); + if (comment is null) + { + throw new CommentNotFoundException(command.CommentId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != comment.StudentId && !identity.IsAdmin) + { + throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); + } + + comment.Delete(); + await _commentRepository.UpdateAsync(comment); + + await _messageBroker.PublishAsync(new CommentDeleted(command.CommentId)); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs new file mode 100644 index 000000000..c17a53909 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/DeleteLikeHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Commands.Handlers +{ + public class DeleteLikeHandler : ICommandHandler + { + private readonly ICommentRepository _commentRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteLikeHandler(ICommentRepository commentRepository, IAppContext appContext, + IMessageBroker messageBroker) + { + _commentRepository = commentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteLike command, CancellationToken cancellationToken = default) + { + var comment = await _commentRepository.GetAsync(command.CommentId); + if (comment is null) + { + throw new CommentNotFoundException(command.CommentId); + } + + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); + } + + comment.UnLike(identity.Id); + await _commentRepository.UpdateAsync(comment); + + await _messageBroker.PublishAsync(new CommentUpdated(command.CommentId)); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/UpdateCommentHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/UpdateCommentHandler.cs new file mode 100644 index 000000000..879d52563 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/UpdateCommentHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Commands.Handlers +{ + public class UpdateCommentHandler : ICommandHandler + { + private readonly ICommentRepository _commentRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + private readonly IDateTimeProvider _dateTimeProvider; + + public UpdateCommentHandler(ICommentRepository commentRepository, IAppContext appContext, + IMessageBroker messageBroker, IDateTimeProvider dateTimeProvider) + { + _commentRepository = commentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + _dateTimeProvider = dateTimeProvider; + } + + public async Task HandleAsync(UpdateComment command, CancellationToken cancellationToken = default) + { + var comment = await _commentRepository.GetAsync(command.CommentId); + if (comment is null) + { + throw new CommentNotFoundException(command.CommentId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != comment.StudentId && !identity.IsAdmin) + { + throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); + } + + comment.Update(command.TextContent, _dateTimeProvider.Now); + await _commentRepository.UpdateAsync(comment); + + await _messageBroker.PublishAsync(new CommentUpdated(command.CommentId)); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs new file mode 100644 index 000000000..34e9bf540 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Commands; +using MiniSpace.Services.Comments.Application.Dto; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class SearchComments : ICommand + { + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid ParentId { get; set; } + public PageableDto Pageable { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/UpdateComment.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/UpdateComment.cs new file mode 100644 index 000000000..e93a0e67d --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/UpdateComment.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Comments.Application.Commands +{ + public class UpdateComment : ICommand + { + public Guid CommentId { get; } + public string TextContent { get; } + + public UpdateComment(Guid commentId, string textContent) + { + CommentId = commentId; + TextContent = textContent; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/ContractAttribute.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/ContractAttribute.cs new file mode 100644 index 000000000..d5b2ceecf --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/ContractAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace MiniSpace.Services.Comments.Application +{ + public class ContractAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs new file mode 100644 index 000000000..72ca781fc --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/CommentDto.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Comments.Core.Entities; + +namespace MiniSpace.Services.Comments.Application.Dto +{ + public class CommentDto + { + public Guid Id { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid StudentId { get; set; } + public string StudentName { get; set; } + public IEnumerable Likes { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdatedAt { get; set; } + public DateTime LastReplyAt { get; set; } + public int RepliesCount { get; set; } + public bool IsDeleted { get; set; } + + public CommentDto() + { + } + + public CommentDto (Comment comment) + { + Id = comment.Id; + ContextId = comment.ContextId; + CommentContext = comment.CommentContext.ToString().ToLowerInvariant(); + StudentId = comment.StudentId; + StudentName = comment.StudentName; + Likes = comment.Likes; + ParentId = comment.ParentId; + TextContent = comment.TextContent; + CreatedAt = comment.CreatedAt; + LastUpdatedAt = comment.LastUpdatedAt; + LastReplyAt = comment.LastReplyAt; + RepliesCount = comment.RepliesCount; + IsDeleted = comment.IsDeleted; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs new file mode 100644 index 000000000..0aa0823b7 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/PageableDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs new file mode 100644 index 000000000..7aaa0fc53 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Dto/SortDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentCreated.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentCreated.cs new file mode 100644 index 000000000..2007156a5 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentCreated.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events +{ + public class CommentCreated : IEvent + { + public Guid CommentId { get; } + + public CommentCreated(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentDeleted.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentDeleted.cs new file mode 100644 index 000000000..d30ad3f1b --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentDeleted.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events +{ + public class CommentDeleted : IEvent + { + public Guid CommentId { get; } + + public CommentDeleted(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentUpdated.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentUpdated.cs new file mode 100644 index 000000000..360c41cc1 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/CommentUpdated.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events +{ + public class CommentUpdated : IEvent + { + public Guid CommentId { get; } + + public CommentUpdated(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/EventDeleted.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/EventDeleted.cs new file mode 100644 index 000000000..d902fb896 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/EventDeleted.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Comments.Application.Events.External +{ + [Message("events")] + public class EventDeleted : IEvent + { + public Guid EventId { get; } + + public EventDeleted(Guid eventId) + { + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/EventDeletedHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/EventDeletedHandler.cs new file mode 100644 index 000000000..9392c392b --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/EventDeletedHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Events.External.Handlers +{ + public class EventDeletedHandler : IEventHandler + { + private readonly ICommentRepository _commentRepository; + + public EventDeletedHandler(ICommentRepository commentRepository) + { + _commentRepository = commentRepository; + } + + public async Task HandleAsync(EventDeleted @event, CancellationToken cancellationToken = default) + { + //if (!(await _eventRepository.ExistsAsync(@event.EventId))) + //{ + // throw new EventNotFoundException(@event.EventId); + //} + + var comments = await _commentRepository.GetByEventIdAsync(@event.EventId); + foreach(var comment in comments) + { + await _commentRepository.DeleteAsync(comment.Id); + } + + //await _eventRepository.DeleteAsync(@event.EventId); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/PostDeletedHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/PostDeletedHandler.cs new file mode 100644 index 000000000..7c69cbf7a --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/PostDeletedHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.Application.Events.External.Handlers +{ + public class PostDeletedHandler : IEventHandler + { + private readonly ICommentRepository _commentRepository; + + public PostDeletedHandler(ICommentRepository commentRepository) + { + _commentRepository = commentRepository; + } + + public async Task HandleAsync(PostDeleted @event, CancellationToken cancellationToken = default) + { + //if (!(await _studentRepository.ExistsAsync(@event.StudentId))) + //{ + // throw new StudentNotFoundException(@event.StudentId); + //} + + var comments = await _commentRepository.GetByPostIdAsync(@event.PostId); + foreach (var comment in comments) + { + await _commentRepository.DeleteAsync(comment.Id); + } + + //await _studentRepository.DeleteAsync(@event.StudentId); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 000000000..19e873bf9 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; + +namespace MiniSpace.Services.Comments.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 StudentAlreadyExistsException(@event.StudentId); + } + + await _studentRepository.AddAsync(new Student(@event.StudentId)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentDeletedHandler.cs similarity index 72% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs rename to MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentDeletedHandler.cs index b2a2514d7..fe9321fe6 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentDeletedHandler.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/Handlers/StudentDeletedHandler.cs @@ -1,8 +1,12 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Convey.CQRS.Events; -using MiniSpace.Services.Posts.Application.Exceptions; -using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Repositories; -namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +namespace MiniSpace.Services.Comments.Application.Events.External.Handlers { public class StudentDeletedHandler : IEventHandler { diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/PostDeleted.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/PostDeleted.cs new file mode 100644 index 000000000..0163b3514 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/PostDeleted.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Comments.Application.Events.External +{ + [Message("posts")] + 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/External/StudentCreated.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentCreated.cs similarity index 82% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentCreated.cs rename to MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentCreated.cs index f7d769110..b85346e58 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentCreated.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentCreated.cs @@ -1,7 +1,8 @@ using Convey.CQRS.Events; using Convey.MessageBrokers; +using System; -namespace MiniSpace.Services.Posts.Application.Events.External +namespace MiniSpace.Services.Comments.Application.Events.External { [Message("students")] public class StudentCreated : IEvent diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentDeleted.cs similarity index 82% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs rename to MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentDeleted.cs index 844641aa5..75f0176af 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/StudentDeleted.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/External/StudentDeleted.cs @@ -1,7 +1,8 @@ using Convey.CQRS.Events; using Convey.MessageBrokers; +using System; -namespace MiniSpace.Services.Posts.Application.Events.External +namespace MiniSpace.Services.Comments.Application.Events.External { [Message("students")] public class StudentDeleted : IEvent diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeDeleted.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeDeleted.cs new file mode 100644 index 000000000..3ffbfc7da --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using System; + + +namespace MiniSpace.Services.Comments.Application.Events +{ + public class LikeDeleted : IEvent + { + public Guid CommentId { get; } + + public LikeDeleted(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeUpdated.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeUpdated.cs new file mode 100644 index 000000000..fdd2ff22a --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/LikeUpdated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using System; + + +namespace MiniSpace.Services.Comments.Application.Events +{ + public class LikeUpdated : IEvent + { + public Guid CommentId { get; } + + public LikeUpdated(Guid commentId) + { + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/CreateCommentRejected.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/CreateCommentRejected.cs new file mode 100644 index 000000000..1ac0d75cc --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/CreateCommentRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events.Rejected +{ + public class CreateCommentRejected : IRejectedEvent + { + public Guid CommentId { get; } + public string Reason { get; } + public string Code { get; } + + public CreateCommentRejected(Guid commentId, string reason, string code) + { + CommentId = commentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/DeleteCommentRejected.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/DeleteCommentRejected.cs new file mode 100644 index 000000000..6e2231517 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/DeleteCommentRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events.Rejected +{ + public class DeleteCommentRejected : IRejectedEvent + { + public Guid CommentId { get; } + public string Reason { get; } + public string Code { get; } + + public DeleteCommentRejected(Guid commentId, string reason, string code) + { + CommentId = commentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/UpdateCommentRejected.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/UpdateCommentRejected.cs new file mode 100644 index 000000000..693a22760 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Events/Rejected/UpdateCommentRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Comments.Application.Events.Rejected +{ + public class UpdateCommentRejected : IRejectedEvent + { + public Guid CommentId { get; } + public string Reason { get; } + public string Code { get; } + + public UpdateCommentRejected(Guid commentId, string reason, string code) + { + CommentId = commentId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/AppException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..a15634a0e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/AppException.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/CommentNotFoundException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/CommentNotFoundException.cs new file mode 100644 index 000000000..ccfbb45ed --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/CommentNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class CommentNotFoundException : AppException + { + public override string Code { get; } = "comment_not_found"; + public Guid Id { get; } + + public CommentNotFoundException(Guid id) : base($"Comment with id: {id} was not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextEnumException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextEnumException.cs new file mode 100644 index 000000000..f539b84e7 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextEnumException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class InvalidCommentContextEnumException : AppException + { + public override string Code { get; } = "invalid_commentcontext_enum"; + public string InvalidEnum { get; } + + public InvalidCommentContextEnumException(string invalidEnum) : base( + $"String: {invalidEnum} cannot be parsed to valid CommentContext.") + { + InvalidEnum = invalidEnum; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextException.cs new file mode 100644 index 000000000..facb2c8b3 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidCommentContextException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class InvalidCommentContextException: AppException + { + public override string Code { get; } = "invalid_comment_context"; + public string Context { get; } + + public InvalidCommentContextException(string context) : base($"Invalid comment context: {context}.") + { + Context = context; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidParentCommentException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidParentCommentException.cs new file mode 100644 index 000000000..5e095cf8a --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/InvalidParentCommentException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class InvalidParentCommentException : AppException + { + public override string Code { get; } = "invalid_parent_comment"; + public Guid ParentId { get; } + + public InvalidParentCommentException(Guid parentId) : base($"Invalid parent comment with id: '{parentId}'. It cannot be a child comment.") + { + ParentId = parentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/ParentComentNotFoundException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/ParentComentNotFoundException.cs new file mode 100644 index 000000000..22cb3d39b --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/ParentComentNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class ParentCommentNotFoundException : AppException + { + public override string Code { get; } = "parent_comment_not_found"; + public Guid ParentId { get; } + + public ParentCommentNotFoundException(Guid parentId) : base($"Parent comment with id: '{parentId}' was not found.") + { + ParentId = parentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentAlreadyExistsException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentAlreadyExistsException.cs new file mode 100644 index 000000000..2b52f6291 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentAlreadyExistsException.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class StudentAlreadyExistsException : AppException + { + public override string Code { get; } = "student_already_added"; + public Guid StudentId { get; } + + public StudentAlreadyExistsException(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.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentNotFoundException.cs similarity index 80% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentNotFoundException.cs rename to MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentNotFoundException.cs index 1f7d6b593..e7e399c6c 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentNotFoundException.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/StudentNotFoundException.cs @@ -1,4 +1,6 @@ -namespace MiniSpace.Services.Posts.Application.Exceptions +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions { public class StudentNotFoundException : AppException { diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/UnauthorizedCommentAccessException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/UnauthorizedCommentAccessException.cs new file mode 100644 index 000000000..5c0c95898 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Exceptions/UnauthorizedCommentAccessException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Exceptions +{ + public class UnauthorizedCommentAccessException : AppException + { + public override string Code { get; } = "unauthorized_comment_access"; + public Guid CommentId { get; } + public Guid UserId { get; } + + public UnauthorizedCommentAccessException(Guid commentId, Guid userId) + : base($"Unauthorized access to comment with id: '{commentId}' by user with id: '{userId}'.") + { + CommentId = commentId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Extensions.cs new file mode 100644 index 000000000..09dbc4ac6 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Application/IAppContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/IAppContext.cs new file mode 100644 index 000000000..8c2c9b106 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Comments.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/IIdentityContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/IIdentityContext.cs new file mode 100644 index 000000000..c1ade755c --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/IIdentityContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Application/MiniSpace.Services.Comments.Application.csproj b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/MiniSpace.Services.Comments.Application.csproj new file mode 100644 index 000000000..6b085f9b4 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/MiniSpace.Services.Comments.Application.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs new file mode 100644 index 000000000..3bbad565c --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Application.Wrappers; + +namespace MiniSpace.Services.Comments.Application.Services +{ + public interface ICommentService + { + Task>> BrowseCommentsAsync(SearchComments command); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..5832fa711 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Services.Comments.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IEventMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..db27763e1 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IEventMapper.cs @@ -0,0 +1,12 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Comments.Core; +using System.Collections.Generic; + +namespace MiniSpace.Services.Comments.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..64c9d4e65 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/IMessageBroker.cs @@ -0,0 +1,12 @@ +using Convey.CQRS.Events; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateId.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..a58b79062 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateId.cs @@ -0,0 +1,51 @@ +using MiniSpace.Services.Comments.Core.Exceptions; +using System; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..753e23dbf --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/AggregateRoot.cs @@ -0,0 +1,20 @@ +using MiniSpace.Services.Comments.Core; +using System.Collections.Generic; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs new file mode 100644 index 000000000..cefd9bf3e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs @@ -0,0 +1,104 @@ +using MiniSpace.Services.Comments.Core.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace MiniSpace.Services.Comments.Core.Entities +{ + public class Comment : AggregateRoot + { + private ISet _likes = new HashSet(); + public Guid ContextId { get; private set; } + public CommentContext CommentContext { get; private set; } + public Guid StudentId { get; private set; } + public string StudentName { get; private set; } + public Guid ParentId { get; private set; } + public string TextContent { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime LastUpdatedAt { get; private set; } + public DateTime LastReplyAt { get; private set; } + public int RepliesCount { get; private set; } + public bool IsDeleted { get; private set; } + + public IEnumerable Likes + { + get => _likes; + private set => _likes = new HashSet(value); + } + + public Comment(Guid id, Guid contextId, CommentContext commentContext, Guid studentId, string studentName, + IEnumerable likes, Guid parentId, string textContent, DateTime createdAt, DateTime lastUpdatedAt, + DateTime lastReplyAt, int repliesCount, bool isDeleted) + { + Id = id; + ContextId = contextId; + CommentContext = commentContext; + StudentId = studentId; + StudentName = studentName; + Likes = likes; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + LastReplyAt = lastReplyAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + + public void Like(Guid studentId) + { + if (Likes.Any(id => id == studentId)) + { + throw new StudentAlreadyLikesCommentException(studentId); + } + _likes.Add(studentId); + } + + public void UnLike(Guid studentId) + { + if (Likes.All(id => id != studentId)) + { + throw new StudentNotLikeCommentException(studentId, Id); + } + _likes.Remove(studentId); + } + + public static Comment Create(AggregateId id, Guid contextId, CommentContext commentContext, Guid studentId, + string studentName, Guid parentId, string textContent, DateTime createdAt) + { + CheckContent(id, textContent); + + return new Comment(id, contextId, commentContext, studentId, studentName, new List(), parentId, textContent, + createdAt, createdAt, createdAt, 0,false); + } + + public void Update(string textContent, DateTime now) + { + CheckContent(Id, textContent); + + TextContent = textContent; + LastUpdatedAt = now; + } + + private static void CheckContent(AggregateId id, string textContent) + { + if (string.IsNullOrWhiteSpace(textContent) || textContent.Length > 300) + { + throw new InvalidCommentContentException(id); + } + } + + public void Delete() + { + IsDeleted = true; + TextContent = ""; + } + + public void AddReply(DateTime now) + { + RepliesCount++; + LastReplyAt = now; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/CommentContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/CommentContext.cs new file mode 100644 index 000000000..866c3b3aa --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/CommentContext.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Core.Entities +{ + public enum CommentContext + { + Post, + Event + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Student.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Student.cs new file mode 100644 index 000000000..b9290f47f --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Student.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Comments.Core.Entities +{ + public class Student + { + public Guid Id { get; private set; } + + public Student(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..0482ff69c --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/DomainException.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..5ec007725 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidCommentContentException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidCommentContentException.cs new file mode 100644 index 000000000..4eaddb6df --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/InvalidCommentContentException.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Services.Comments.Core.Exceptions +{ + public class InvalidCommentContentException : DomainException + { + public override string Code { get; } = "invalid_comment_content"; + public Guid Id { get; } + + public InvalidCommentContentException(Guid id) : base( + $"Comment with id: {id} has invalid content.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentAlreadyLikesCommentException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentAlreadyLikesCommentException.cs new file mode 100644 index 000000000..d8f4359df --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentAlreadyLikesCommentException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Core.Exceptions +{ + public class StudentAlreadyLikesCommentException : DomainException + { + public override string Code { get; } = "student_already_likes_comment"; + public Guid Id { get; } + + public StudentAlreadyLikesCommentException(Guid id) : base( + $"Comment with id: {id} has already liked this comment.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentNotLikeCommentException.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentNotLikeCommentException.cs new file mode 100644 index 000000000..ef274af44 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Exceptions/StudentNotLikeCommentException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Core.Exceptions +{ + public class StudentNotLikeCommentException : DomainException + { + public override string Code { get; } = "student_not_likes_comment"; + public Guid StudentId { get; } + public Guid CommentId { get; } + + public StudentNotLikeCommentException(Guid studentId, Guid commentId) : base( + $"Student with id: {studentId} does not like comment with id:.") + { + StudentId = studentId; + CommentId = commentId; + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/IDomainEvent.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/IDomainEvent.cs new file mode 100644 index 000000000..ce8577480 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/IDomainEvent.cs @@ -0,0 +1,6 @@ +namespace MiniSpace.Services.Comments.Core +{ + public interface IDomainEvent + { + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/MiniSpace.Services.Comments.Core.csproj b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/MiniSpace.Services.Comments.Core.csproj new file mode 100644 index 000000000..3c20077e9 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/MiniSpace.Services.Comments.Core.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + disable + + + diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/ICommentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/ICommentRepository.cs new file mode 100644 index 000000000..20ea99c79 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/ICommentRepository.cs @@ -0,0 +1,22 @@ +using MiniSpace.Services.Comments.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.Core.Repositories +{ + public interface ICommentRepository + { + Task GetAsync(Guid id); + Task AddAsync(Comment comment); + Task UpdateAsync(Comment comment); + Task DeleteAsync(Guid id); + Task> GetByEventIdAsync(Guid eventId); + Task> GetByPostIdAsync(Guid postId); + + Task<(IEnumerable comments, int pageNumber, int pageSize, int totalPages, int totalElements)> + BrowseCommentsAsync(int pageNumber, int pageSize, Guid contextId, CommentContext context, Guid parentId, + IEnumerable sortBy, string direction); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..5043e91dd --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Comments.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..8872f116e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs new file mode 100644 index 000000000..016d8ffe6 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Wrappers/Response.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..9a23e58cc --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Comments.Application; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..6278674e3 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Comments.Application; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..a86d3ee2d --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..12d43b7d7 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Comments.Application; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..988cdbf37 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.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.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..00e34126e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.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.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..fdde1c6f7 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,86 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Events.Rejected; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Exceptions; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..cf4883382 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Core.Exceptions; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs new file mode 100644 index 000000000..38e07cf93 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs @@ -0,0 +1,136 @@ +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.Comments.Application; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Events.External; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Infrastructure.Contexts; +using MiniSpace.Services.Comments.Infrastructure.Decorators; +using MiniSpace.Services.Comments.Infrastructure.Exceptions; +using MiniSpace.Services.Comments.Infrastructure.Logging; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Comments.Infrastructure.Services; + +namespace MiniSpace.Services.Comments.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(); + 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") + .AddMongoRepository("comments") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .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.Comments/src/MiniSpace.Services.Comments.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..a66e221ae --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Comments.Application; + +namespace MiniSpace.Services.Comments.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..777426b5e --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Comments.Application.Commands; + +namespace MiniSpace.Services.Comments.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UpdateComment).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..77c86f651 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,63 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Events; +using MiniSpace.Services.Comments.Application.Events.External; + +namespace MiniSpace.Services.Comments.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(CreateComment), new HandlerLogTemplate + { + After = "Created the comment with id: {CommentId}." + } + }, + { + typeof(UpdateComment), new HandlerLogTemplate + { + After = "Updated the comment with id: {CommentId}." + } + }, + { + typeof(DeleteComment), new HandlerLogTemplate + { + After = "Deleted the comment with id: {CommentId}." + } + }, + { + typeof(StudentCreated), new HandlerLogTemplate + { + After = "Created a new student with id: {StudentId}." + } + }, + { + typeof(StudentDeleted), new HandlerLogTemplate + { + After = "Deleted a student with id: {StudentId}." + } + }, + { + typeof(AddLike), new HandlerLogTemplate + { + After = "Added like in the comment with id: {CommentId}." + } + }, + { + typeof(DeleteLike), new HandlerLogTemplate + { + After = "Removed like in the comment with id: {CommentId}." + } + }, + }; + + 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.Comments/src/MiniSpace.Services.Comments.Infrastructure/MiniSpace.Services.Comments.Infrastructure.csproj b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/MiniSpace.Services.Comments.Infrastructure.csproj new file mode 100644 index 000000000..11a87d0b2 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/MiniSpace.Services.Comments.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + disable + MiniSpace.Services.Comments.Infrastructure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs new file mode 100644 index 000000000..d6e3e9066 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -0,0 +1,22 @@ +using Convey.Types; +using MiniSpace.Services.Comments.Core.Entities; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents +{ + public class CommentDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid ContextId { get; set; } + public CommentContext CommentContext { get; set; } + public Guid StudentId { get; set; } + public string StudentName { get; set; } + public IEnumerable Likes { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdatedAt { get; set; } + public DateTime LastReplyAt { get; set; } + public int RepliesCount { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..5d02210e9 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,58 @@ +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Core.Entities; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Comment AsEntity(this CommentDocument document) + => new Comment(document.Id,document.ContextId,document.CommentContext, document.StudentId, + document.StudentName, document.Likes, document.ParentId, document.TextContent, document.CreatedAt, + document.LastUpdatedAt, document.LastReplyAt, document.RepliesCount, document.IsDeleted); + + public static CommentDocument AsDocument(this Comment entity) + => new CommentDocument() + { + Id = entity.Id, + ContextId = entity.ContextId, + CommentContext = entity.CommentContext, + StudentId = entity.StudentId, + StudentName = entity.StudentName, + Likes = entity.Likes, + ParentId = entity.ParentId, + TextContent = entity.TextContent, + CreatedAt = entity.CreatedAt, + LastUpdatedAt = entity.LastUpdatedAt, + LastReplyAt = entity.LastReplyAt, + RepliesCount = entity.RepliesCount, + IsDeleted = entity.IsDeleted, + }; + + public static CommentDto AsDto(this CommentDocument document) + => new CommentDto() + { + Id = document.Id, + ContextId = document.ContextId, + CommentContext = document.CommentContext.ToString().ToLowerInvariant(), + StudentId = document.StudentId, + StudentName = document.StudentName, + Likes = document.Likes, + ParentId = document.ParentId, + TextContent = document.TextContent, + CreatedAt = document.CreatedAt, + LastUpdatedAt = document.LastUpdatedAt, + LastReplyAt = document.LastReplyAt, + RepliesCount = document.RepliesCount, + IsDeleted= document.IsDeleted, + }; + + public static Student AsEntity(this StudentDocument document) + => new Student(document.Id); + + public static StudentDocument AsDocument(this Student entity) + => new StudentDocument + { + Id = entity.Id, + }; + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..eea1b15dd --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Documents +{ + public class StudentDocument : IIdentifiable + { + public Guid Id { get; set; } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs new file mode 100644 index 000000000..14afb264f --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/CommentMongoRepository.cs @@ -0,0 +1,77 @@ +using Convey.Persistence.MongoDB; +using Microsoft.Extensions.Hosting; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories +{ + public class CommentMongoRepository : ICommentRepository + { + private readonly IMongoRepository _repository; + + public CommentMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var comment = await _repository.GetAsync(p => p.Id == id); + + return comment?.AsEntity(); + } + + public Task AddAsync(Comment comment) + => _repository.AddAsync(comment.AsDocument()); + + public Task UpdateAsync(Comment comment) + => _repository.UpdateAsync(comment.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + + public async Task> GetByEventIdAsync(Guid eventId) + { + var comments = _repository.Collection.AsQueryable(); + var commentsByEventId = await comments.Where(e =>e.CommentContext == CommentContext.Event && e.ContextId == eventId).ToListAsync(); + return commentsByEventId.Select(e => e.AsEntity()); + } + + public async Task> GetByPostIdAsync(Guid postId) + { + var comments = _repository.Collection.AsQueryable(); + var commentsByEventId = await comments.Where(e => e.CommentContext == CommentContext.Post && e.ContextId == postId).ToListAsync(); + return commentsByEventId.Select(e => e.AsEntity()); + } + + private async Task<(int totalPages, int totalElements, IEnumerable data)> BrowseAsync( + FilterDefinition filterDefinition, SortDefinition sortDefinition, + int pageNumber, int pageSize) + { + var pagedEvents = await _repository.Collection.AggregateByPage( + filterDefinition, + sortDefinition, + pageNumber, + pageSize); + + return pagedEvents; + } + + public async Task<(IEnumerable comments, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseCommentsAsync(int pageNumber, int pageSize, + Guid contextId, CommentContext context, Guid parentId, IEnumerable sortBy, string direction) + { + var filterDefinition = parentId == Guid.Empty + ? Extensions.ToFilterDefinition(contextId, context).AddParentFilter() + : Extensions.ToFilterDefinition(contextId, context).AddChildrenFilter(parentId); + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); + + return (pagedEvents.data.Select(e => e.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); + } + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs new file mode 100644 index 000000000..713d611db --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/Extensions.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories +{ + public static class Extensions + { + private static readonly FilterDefinitionBuilder FilterDefinitionBuilder = Builders.Filter; + public static async Task<(int totalPages, int totalElements, IReadOnlyList data)> AggregateByPage( + this IMongoCollection collection, + FilterDefinition filterDefinition, + SortDefinition sortDefinition, + int page, + int pageSize) + { + var countFacet = AggregateFacet.Create("count", + PipelineDefinition.Create(new[] + { + PipelineStageDefinitionBuilder.Count() + })); + + var dataFacet = AggregateFacet.Create("data", + PipelineDefinition.Create(new[] + { + PipelineStageDefinitionBuilder.Sort(sortDefinition), + PipelineStageDefinitionBuilder.Skip((page - 1) * pageSize), + PipelineStageDefinitionBuilder.Limit(pageSize), + })); + + + var aggregation = await collection.Aggregate() + .Match(filterDefinition) + .Facet(countFacet, dataFacet) + .ToListAsync(); + + var count = aggregation.First() + .Facets.First(x => x.Name == "count") + .Output() + ?.FirstOrDefault() + ?.Count; + + if (count == null) + { + return (0, 0, Array.Empty()); + } + var totalPages = (int)Math.Ceiling((double)count / pageSize); + + var data = aggregation.First() + .Facets.First(x => x.Name == "data") + .Output(); + + return (totalPages, (int)count, data); + } + + public static FilterDefinition ToFilterDefinition(Guid contextId, CommentContext context) + { + var filterDefinition = FilterDefinitionBuilder.Empty; + + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.ContextId, contextId); + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.CommentContext, context); + + return filterDefinition; + } + + public static FilterDefinition AddParentFilter (this FilterDefinition filterDefinition) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.ParentId, Guid.Empty); + return filterDefinition; + } + + public static FilterDefinition AddChildrenFilter (this FilterDefinition filterDefinition, + Guid parentId) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.ParentId, parentId); + return filterDefinition; + } + + public static SortDefinition ToSortDefinition(IEnumerable sortByArguments, string direction) + { + var sort = sortByArguments.ToList(); + if(!sort.Any()) + { + sort.Add("LastReplyAt"); + sort.Add("LastUpdatedAt"); + } + 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.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs similarity index 77% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs rename to MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs index 4f4e84eaf..6fd9a4fe2 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -1,9 +1,9 @@ using Convey.Persistence.MongoDB; -using MiniSpace.Services.Posts.Core.Entities; -using MiniSpace.Services.Posts.Core.Repositories; -using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; -namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories { public class StudentMongoRepository : IStudentRepository { diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs new file mode 100644 index 000000000..b51edaa99 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs @@ -0,0 +1,43 @@ +using MiniSpace.Services.Comments.Application; +using MiniSpace.Services.Comments.Application.Commands; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Application.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Application.Wrappers; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Comments.Infrastructure.Services +{ + public class CommentService : ICommentService + { + private readonly ICommentRepository _commentRepository; + + public CommentService(ICommentRepository commentRepository) + { + _commentRepository = commentRepository; + } + + public async Task>> BrowseCommentsAsync(SearchComments command) + { + if (!Enum.TryParse(command.CommentContext, true, out var context)) + { + throw new InvalidCommentContextException(command.CommentContext); + } + + var pageNumber = command.Pageable.Page < 1 ? 1 : command.Pageable.Page; + var pageSize = command.Pageable.Size > 10 ? 10 : command.Pageable.Size; + + var result = await _commentRepository.BrowseCommentsAsync( + pageNumber, pageSize, command.ContextId, context, command.ParentId, + command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); + + var pagedEvents = new PagedResponse>( + result.comments.Select(c => new CommentDto(c)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + return pagedEvents; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..4b356528a --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Comments.Application.Services; + +namespace MiniSpace.Services.Comments.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..357db5bef --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,22 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Core; + +namespace MiniSpace.Services.Comments.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.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..c4355c2a4 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.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.Comments.Application.Services; + +namespace MiniSpace.Services.Comments.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.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs index baf7c85bc..c1b8e8662 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -36,21 +36,29 @@ public static async Task Main(string[] args) .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) .Post("events/search", async (cmd, ctx) => { - var pagedResult = await ctx.RequestServices.GetService().SignInAsync(cmd); + var pagedResult = await ctx.RequestServices.GetService().BrowseEventsAsync(cmd); + await ctx.Response.WriteJsonAsync(pagedResult); + }) + .Post("events/search/organizer", async (cmd, ctx) => + { + var pagedResult = await ctx.RequestServices.GetService().BrowseOrganizerEventsAsync(cmd); await ctx.Response.WriteJsonAsync(pagedResult); })) .UseDispatcherEndpoints(endpoints => endpoints .Get("events/{eventId}") - //.Get>("events/organizer/{organizerId}") - //.Put("events/{eventId}") + .Put("events/{eventId}") .Post("events", afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) + .Delete("events/{eventId}") .Post("events/{eventId}/sign-up") + .Delete("events/{eventId}/sign-up") .Post("events/{eventId}/show-interest") + .Delete("events/{eventId}/show-interest") .Post("events/{eventId}/rate") - // TODO: Add query for student latest enrolled events .Get>>("events/student/{studentId}") - .Delete("events/{eventId}") + .Get("events/{eventId}/participants") + .Post("events/{eventId}/participants") + .Delete("events/{eventId}/participants") ) ) .UseLogging() 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 index 187d80747..18ad866e0 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.local.json +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/appsettings.local.json @@ -24,7 +24,9 @@ "type": "direct", "retries": 3, "services": { - "students": "http://localhost:5007" + "students": "http://localhost:5007", + "friends": "http://localhost:5012", + "organizations": "http://localhost:5015" }, "requestMasking": { "enabled": true, 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 index cfb4d59ae..8573c7592 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs @@ -9,6 +9,7 @@ public class AddEvent : ICommand public Guid EventId { get; } public string Name { get; } public Guid OrganizerId { get; } + public Guid OrganizationId { get; } public string StartDate { get; } public string EndDate { get; } public string BuildingName { get; } @@ -23,13 +24,14 @@ public class AddEvent : ICommand 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) + public AddEvent(Guid eventId, string name, Guid organizerId, Guid organizationId, 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; + OrganizationId = organizationId; StartDate = startDate; EndDate = endDate; BuildingName = buildingName; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEventParticipant.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEventParticipant.cs new file mode 100644 index 000000000..596a3c2ea --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEventParticipant.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class AddEventParticipant: ICommand + { + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + public string StudentName { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelInterestInEvent.cs new file mode 100644 index 000000000..c88c1157e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelInterestInEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class CancelInterestInEvent: ICommand + { + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + + public CancelInterestInEvent(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/CancelSignUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelSignUpToEvent.cs new file mode 100644 index 000000000..4ca8d5a6f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CancelSignUpToEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class CancelSignUpToEvent : ICommand + { + public Guid EventId { get; set; } + public Guid StudentId { get; set; } + + public CancelSignUpToEvent(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/Handlers/AddEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs index 9dfebeb32..9c284e67b 100644 --- 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 @@ -6,6 +6,7 @@ using MiniSpace.Services.Events.Application.Events; using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; @@ -15,17 +16,18 @@ public class AddEventHandler: ICommandHandler { private readonly IEventRepository _eventRepository; private readonly IMessageBroker _messageBroker; - private readonly IEventMapper _eventMapper; + private readonly IOrganizationsServiceClient _organizationsServiceClient; 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) + public AddEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, + IOrganizationsServiceClient organizationsServiceClient, IDateTimeProvider dateTimeProvider, + IEventValidator eventValidator, IAppContext appContext) { _eventRepository = eventRepository; _messageBroker = messageBroker; - _eventMapper = eventMapper; + _organizationsServiceClient = organizationsServiceClient; _dateTimeProvider = dateTimeProvider; _eventValidator = eventValidator; _appContext = appContext; @@ -39,27 +41,43 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo if(identity.Id != command.OrganizerId) throw new OrganizerCannotAddEventForAnotherOrganizerException(identity.Id, command.OrganizerId); - var category = _eventValidator.ParseCategory(command.Category); + _eventValidator.ValidateName(command.Name); + _eventValidator.ValidateDescription(command.Description); 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 address = new Address(command.BuildingName, command.Street, command.BuildingNumber, + command.ApartmentNumber, command.City, command.ZipCode); + _eventValidator.ValidateCapacity(command.Capacity); + _eventValidator.ValidateFee(command.Fee); + var category = _eventValidator.ParseCategory(command.Category); var publishDate = now; - var status = State.Published; - if (command.PublishDate != null) + var state = State.Published; + if (command.PublishDate != string.Empty) { publishDate = _eventValidator.ParseDate(command.PublishDate, "event_publish_date"); _eventValidator.ValidateDates(now, publishDate, "now", "event_publish_date"); - status = State.ToBePublished; + _eventValidator.ValidateDates(publishDate, startDate, "event_publish_date", "event_start_date"); + state = 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 organization = await _organizationsServiceClient.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + if (!organization.Organizers.Contains(command.OrganizerId)) + { + throw new OrganizerDoesNotBelongToOrganizationException(command.OrganizerId, command.OrganizationId); + } + + var organizer = new Organizer(command.OrganizerId, identity.Name, identity.Email, command.OrganizerId, organization.Name); var @event = Event.Create(command.EventId, command.Name, command.Description, startDate, endDate, - address, command.Capacity, command.Fee, category, status, publishDate, organizer); + address, command.Capacity, command.Fee, category, state, publishDate, organizer, now); await _eventRepository.AddAsync(@event); await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id)); diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventParticipantHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventParticipantHandler.cs new file mode 100644 index 000000000..48c4fe35a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventParticipantHandler.cs @@ -0,0 +1,56 @@ +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 AddEventParticipantHandler: ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IStudentRepository _studentRepository; + private readonly IAppContext _appContext; + private IMessageBroker _messageBroker; + + public AddEventParticipantHandler(IEventRepository eventRepository, IStudentRepository studentRepository, + IAppContext appContext, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _studentRepository = studentRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AddEventParticipant 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); + } + + var identity = _appContext.Identity; + if(identity.IsAuthenticated && @event.Organizer.Id != identity.Id && !identity.IsAdmin) + { + throw new UnauthorizedEventAccessException(@event.Id, identity.Id); + } + + @event.AddParticipant(new Participant(command.StudentId, command.StudentName)); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new EventParticipantAdded(@event.Id, + command.StudentId, command.StudentName)); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelInterestInEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelInterestInEventHandler.cs new file mode 100644 index 000000000..e92c5834b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelInterestInEventHandler.cs @@ -0,0 +1,44 @@ +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 CancelInterestInEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public CancelInterestInEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, + IAppContext appContext) + { + _eventRepository = eventRepository; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(CancelInterestInEvent 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); + } + + @event.CancelInterest(command.StudentId); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new StudentCancelledInterestInEvent(@event.Id, command.StudentId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelSignUpToEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelSignUpToEventHandler.cs new file mode 100644 index 000000000..a73533d97 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CancelSignUpToEventHandler.cs @@ -0,0 +1,44 @@ +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 CancelSignUpToEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public CancelSignUpToEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, + IAppContext appContext) + { + _eventRepository = eventRepository; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(CancelSignUpToEvent 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); + } + + @event.CancelSignUp(command.StudentId); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new StudentCancelledSignUpToEvent(@event.Id, command.StudentId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RemoveEventParticipantHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RemoveEventParticipantHandler.cs new file mode 100644 index 000000000..297384d56 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/RemoveEventParticipantHandler.cs @@ -0,0 +1,45 @@ +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.Repositories; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class RemoveEventParticipantHandler: ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IAppContext _appContext; + private IMessageBroker _messageBroker; + + public RemoveEventParticipantHandler(IEventRepository eventRepository, IAppContext appContext, IMessageBroker messageBroker) + { + _eventRepository = eventRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(RemoveEventParticipant 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 && @event.Organizer.Id != identity.Id && !identity.IsAdmin) + { + throw new UnauthorizedEventAccessException(@event.Id, identity.Id); + } + + @event.RemoveParticipant(command.ParticipantId); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new EventParticipantRemoved(@event.Id, command.ParticipantId)); + } + + } +} \ 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 index 559a995c2..3e7a84ccb 100644 --- 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 @@ -46,7 +46,7 @@ public async Task HandleAsync(ShowInterestInEvent command, CancellationToken can throw new StudentNotFoundException(command.StudentId); } - var participant = new Participant(student.Id, identity.Name, identity.Email); + var participant = new Participant(student.Id, identity.Name); @event.ShowStudentInterest(participant); await _eventRepository.UpdateAsync(@event); await _messageBroker.PublishAsync(new StudentShowedInterestInEvent(@event.Id, student.Id)); 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 index cc94246eb..3a3b697ab 100644 --- 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 @@ -47,7 +47,7 @@ public async Task HandleAsync(SignUpToEvent command, CancellationToken cancellat throw new StudentNotFoundException(command.StudentId); } - var participant = new Participant(student.Id, identity.Name, identity.Email); + var participant = new Participant(student.Id, identity.Name); @event.SignUpStudent(participant); await _eventRepository.UpdateAsync(@event); await _messageBroker.PublishAsync(new StudentSignedUpToEvent(@event.Id, student.Id)); diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs new file mode 100644 index 000000000..de8ea2789 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs @@ -0,0 +1,77 @@ +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 UpdateEventHandler : ICommandHandler + { + private readonly IEventRepository _eventRepository; + private readonly IEventValidator _eventValidator; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + private readonly IDateTimeProvider _dateTimeProvider; + + public UpdateEventHandler(IEventRepository eventRepository, IEventValidator eventValidator, + IAppContext appContext, IMessageBroker messageBroker, IDateTimeProvider dateTimeProvider) + { + _eventRepository = eventRepository; + _eventValidator = eventValidator; + _appContext = appContext; + _messageBroker = messageBroker; + _dateTimeProvider = dateTimeProvider; + } + + public async Task HandleAsync(UpdateEvent 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 && !@event.IsOrganizer(identity.Id) && !identity.IsAdmin) + { + throw new UnauthorizedEventAccessException(@event.Id, identity.Id); + } + + var name = command.Name == string.Empty ? @event.Name : command.Name; + var description = command.Description == string.Empty ? @event.Description : command.Description; + var category = command.Category == string.Empty ? @event.Category : _eventValidator.ParseCategory(command.Category); + var startDate = command.StartDate == string.Empty ? @event.StartDate + : _eventValidator.ParseDate(command.StartDate, "event_start_date"); + var endDate = command.EndDate == string.Empty ? @event.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 address = @event.Location.Update(command.BuildingName, command.Street, command.BuildingNumber, + command.ApartmentNumber, command.City, command.ZipCode); + var capacity = command.Capacity == 0 ? @event.Capacity : command.Capacity; + _eventValidator.ValidateUpdatedCapacity(capacity, @event.Capacity); + var fee = command.Fee == 0 ? @event.Fee : command.Fee; + _eventValidator.ValidateUpdatedFee(fee, @event.Fee); + + var publishDate = @event.PublishDate; + var state = @event.State; + if (command.PublishDate != string.Empty) + { + publishDate = _eventValidator.ParseDate(command.PublishDate, "event_publish_date"); + _eventValidator.ValidateDates(now, publishDate, "now", "event_publish_date"); + _eventValidator.ValidateDates(publishDate, startDate, "event_publish_date", "event_start_date"); + state = State.ToBePublished; + } + + @event.Update(name, description, startDate, endDate, address, capacity, fee, category, state, publishDate, now); + await _eventRepository.UpdateAsync(@event); + await _messageBroker.PublishAsync(new EventUpdated(@event.Id, _dateTimeProvider.Now, identity.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RemoveEventParticipant.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RemoveEventParticipant.cs new file mode 100644 index 000000000..679c46efb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/RemoveEventParticipant.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class RemoveEventParticipant: ICommand + { + public Guid EventId { get; set; } + public Guid ParticipantId { 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 index 1b9ef6791..96f8ea8fa 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchEvents.cs @@ -1,4 +1,6 @@ -using Convey.CQRS.Commands; +using System; +using System.Collections.Generic; +using Convey.CQRS.Commands; using MiniSpace.Services.Events.Application.DTO; namespace MiniSpace.Services.Events.Application.Commands @@ -7,6 +9,10 @@ public class SearchEvents : ICommand { public string Name { get; set; } public string Organizer { get; set; } + public string Category { get; set; } + public string State { get; set; } + public IEnumerable Friends { get; set; } + public string FriendsEngagementType { get; set; } public string DateFrom { get; set; } public string DateTo { get; set; } public PageableDto Pageable { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchOrganizerEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchOrganizerEvents.cs new file mode 100644 index 000000000..e51c824fd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/SearchOrganizerEvents.cs @@ -0,0 +1,16 @@ +using System; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class SearchOrganizerEvents : ICommand + { + public string Name { get; set; } + public Guid OrganizerId { get; set; } + public string DateFrom { get; set; } + public string DateTo { get; set; } + public string State { 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/UpdateEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs new file mode 100644 index 000000000..7cc856df7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs @@ -0,0 +1,47 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class UpdateEvent : 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 UpdateEvent(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; + 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/DTO/EventDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs index 0f104d13b..aa1352062 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs @@ -14,7 +14,6 @@ public class EventDto 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; } @@ -24,12 +23,18 @@ public class EventDto public string Category { get; set; } public string Status { get; set; } public DateTime PublishDate { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsSignedUp { get; set; } + public bool IsInterested { get; set; } + public bool HasRated { get; set; } + public IEnumerable FriendsInterestedIn { get; set; } + public IEnumerable FriendsSignedUp { get; set; } public EventDto() { } - public EventDto(Event @event) + public EventDto(Event @event, Guid studentId) { Id = @event.Id; Name = @event.Name; @@ -37,7 +42,6 @@ public EventDto(Event @event) 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(); @@ -46,6 +50,11 @@ public EventDto(Event @event) Category = @event.Category.ToString(); Status = @event.State.ToString(); PublishDate = @event.PublishDate; + IsSignedUp = @event.SignedUpStudents.Any(x => x.StudentId == studentId); + IsInterested = @event.InterestedStudents.Any(x => x.StudentId == studentId); + HasRated = @event.Ratings.Any(x => x.StudentId == studentId); + FriendsInterestedIn = Enumerable.Empty(); + FriendsSignedUp = Enumerable.Empty(); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventParticipantsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventParticipantsDto.cs new file mode 100644 index 000000000..81771c496 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventParticipantsDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class EventParticipantsDto + { + public Guid EventId { get; set; } + public IEnumerable InterestedStudents { get; set; } + public IEnumerable SignedUpStudents { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs new file mode 100644 index 000000000..0858caaae --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class FriendDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public Guid FriendId { get; set; } + public DateTime CreatedAt { get; set; } + public string FriendState { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizationDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizationDto.cs new file mode 100644 index 000000000..23d101a9f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizationDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class OrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public IEnumerable Organizers { get; set; } + } +} \ 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 index bbdb13ef7..d7ee683d3 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizerDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/OrganizerDto.cs @@ -8,7 +8,8 @@ public class OrganizerDto public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } - public string Organization { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } public OrganizerDto() { @@ -19,7 +20,8 @@ public OrganizerDto(Organizer organizer) Id = organizer.Id; Name = organizer.Name; Email = organizer.Email; - Organization = organizer.Organization; + OrganizationId = organizer.OrganizationId; + OrganizationName = organizer.OrganizationName; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ParticipantDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ParticipantDto.cs new file mode 100644 index 000000000..dbf2c39eb --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ParticipantDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class ParticipantDto + { + public Guid StudentId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs new file mode 100644 index 000000000..4dba38c16 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantAdded.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventParticipantAdded: IEvent + { + public Guid EventId { get; } + public Guid ParticipantId { get; } + public string ParticipantName { get; } + + public EventParticipantAdded(Guid eventId, Guid participantId, string participantName) + { + EventId = eventId; + ParticipantId = participantId; + ParticipantName = participantName; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs new file mode 100644 index 000000000..7a4a8d091 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventParticipantRemoved.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventParticipantRemoved: IEvent + { + public Guid EventId { get; } + public Guid Participant { get; } + + public EventParticipantRemoved(Guid eventId, Guid participant) + { + EventId = eventId; + Participant = participant; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs new file mode 100644 index 000000000..c0e79da44 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs @@ -0,0 +1,12 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy) : IEvent + { + public Guid EventId { get; set; } = eventId; + public DateTime UpdatedAt { get; set; } = updatedAt; + public Guid UpdatedBy { get; set; } = updatedBy; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventParticipantRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventParticipantRejected.cs new file mode 100644 index 000000000..896285dc6 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventParticipantRejected.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class AddEventParticipantRejected: IRejectedEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public AddEventParticipantRejected(Guid eventId, Guid studentId, string reason, string code) + { + EventId = eventId; + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} \ 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 index 0184bbe9d..54e972866 100644 --- 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 @@ -5,9 +5,9 @@ namespace MiniSpace.Services.Events.Application.Events.Rejected { - public class AddEventRejected(Guid userId, string reason, string code) : IRejectedEvent + public class AddEventRejected(Guid organizerId, string reason, string code) : IRejectedEvent { - public Guid UserId { get; } = userId; + public Guid OrganizerId { get; } = organizerId; public string Reason { get; } = reason; public string Code { get; } = code; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelInterestInEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelInterestInEventRejected.cs new file mode 100644 index 000000000..ced107d1c --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelInterestInEventRejected.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class CancelInterestInEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public string Reason { get; } + public string Code { get; } + + public CancelInterestInEventRejected(Guid eventId, string reason, string code) + { + EventId = eventId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelSignUpToEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelSignUpToEventRejected.cs new file mode 100644 index 000000000..f5d0ddb8f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CancelSignUpToEventRejected.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class CancelSignUpToEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public string Reason { get; } + public string Code { get; } + + public CancelSignUpToEventRejected(Guid eventId, string reason, string code) + { + EventId = eventId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/DeleteEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/DeleteEventRejected.cs new file mode 100644 index 000000000..082010765 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/DeleteEventRejected.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class DeleteEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public string Reason { get; } + public string Code { get; } + + public DeleteEventRejected(Guid eventId, string reason, string code) + { + EventId = eventId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RateEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RateEventRejected.cs new file mode 100644 index 000000000..2a610238a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RateEventRejected.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class RateEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public RateEventRejected(Guid eventId, Guid studentId, string reason, string code) + { + EventId = eventId; + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RemoveEventParticipantRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RemoveEventParticipantRejected.cs new file mode 100644 index 000000000..5e05cbdbf --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/RemoveEventParticipantRejected.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class RemoveEventParticipantRejected: IRejectedEvent + { + public Guid EventId { get; } + public string Reason { get; } + public string Code { get; } + + public RemoveEventParticipantRejected(Guid eventId, string reason, string code) + { + EventId = eventId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchEventsRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchEventsRejected.cs new file mode 100644 index 000000000..2dbd42582 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchEventsRejected.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class SearchEventsRejected: IRejectedEvent + { + public string Reason { get; } + public string Code { get; } + + public SearchEventsRejected(string reason, string code) + { + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchOrganizerEventsRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchOrganizerEventsRejected.cs new file mode 100644 index 000000000..53c49333b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SearchOrganizerEventsRejected.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class SearchOrganizerEventsRejected: IRejectedEvent + { + public string Reason { get; } + public string Code { get; } + + public SearchOrganizerEventsRejected(string reason, string code) + { + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/ShowInterestInEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/ShowInterestInEventRejected.cs new file mode 100644 index 000000000..bb6e599b0 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/ShowInterestInEventRejected.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class ShowInterestInEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public ShowInterestInEventRejected(Guid eventId, Guid studentId, string reason, string code) + { + EventId = eventId; + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SignUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SignUpToEvent.cs new file mode 100644 index 000000000..537014622 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/SignUpToEvent.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class SignUpToEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + public string Reason { get; } + public string Code { get; } + + public SignUpToEventRejected(Guid eventId, Guid studentId, string reason, string code) + { + EventId = eventId; + StudentId = studentId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/UpdateEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/UpdateEventRejected.cs new file mode 100644 index 000000000..db2c3eb71 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/UpdateEventRejected.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events.Rejected +{ + public class UpdateEventRejected: IRejectedEvent + { + public Guid EventId { get; } + public string Reason { get; } + public string Code { get; } + + public UpdateEventRejected(Guid eventId, string reason, string code) + { + EventId = eventId; + Reason = reason; + Code = code; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs new file mode 100644 index 000000000..a993018b5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledInterestInEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class StudentCancelledInterestInEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentCancelledInterestInEvent(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/StudentCancelledSignUpToEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs new file mode 100644 index 000000000..fdfd41431 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/StudentCancelledSignUpToEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Events.Application.Events +{ + public class StudentCancelledSignUpToEvent: IEvent + { + public Guid EventId { get; } + public Guid StudentId { get; } + + public StudentCancelledSignUpToEvent(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/InvalidEventCapacityException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventCapacityException.cs new file mode 100644 index 000000000..01083e0d8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventCapacityException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventCapacityException : AppException + { + public override string Code { get; } = "invalid_event_capacity"; + public int Capacity { get; } + + public InvalidEventCapacityException(int capacity) : base($"Invalid event capacity: {capacity}. It must be between 1 and 1000.") + { + Capacity = capacity; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDescriptionException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDescriptionException.cs new file mode 100644 index 000000000..6934dbcb3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventDescriptionException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventDescriptionException : AppException + { + public override string Code { get; } = "invalid_event_description"; + public string Description { get; } + + public InvalidEventDescriptionException(string description): base("Invalid event description. It cannot be empty or longer than 5000 characters.") + { + Description = description; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventEngagementTypeException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventEngagementTypeException.cs new file mode 100644 index 000000000..727634904 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventEngagementTypeException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventEngagementTypeException : AppException + { + public override string Code { get; } = "invalid_event_engagement_type"; + public string EngagementType { get; } + + public InvalidEventEngagementTypeException(string engagementType) + : base($"Invalid event engagement type: {engagementType}.") + { + EngagementType = engagementType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventFeeException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventFeeException.cs new file mode 100644 index 000000000..c6b99f883 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventFeeException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventFeeException : AppException + { + public override string Code { get; } = "invalid_event_fee"; + public decimal Fee { get; } + + public InvalidEventFeeException(decimal fee) : base($"Invalid event fee: {fee}. It must be between 0 and 1000.") + { + Fee = fee; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventNameException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventNameException.cs new file mode 100644 index 000000000..c2db3eaee --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventNameException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventNameException : AppException + { + public override string Code { get; } = "invalid_event_name"; + public string Name { get; } + + public InvalidEventNameException(string name): base("Invalid event name. It cannot be empty or longer than 300 characters.") + { + Name = name; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventStateException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventStateException.cs new file mode 100644 index 000000000..ca4f6b106 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventStateException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventStateException : AppException + { + public override string Code { get; } = "invalid_event_state"; + public string State { get; } + + public InvalidEventStateException(string state) : base($"Event State property is invalid: {state}.") + { + State = state; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventCapacityException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventCapacityException.cs new file mode 100644 index 000000000..65adf652d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventCapacityException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidUpdatedEventCapacityException : AppException + { + public override string Code { get; } = "invalid_updated_event_capacity"; + public int CurrentCapacity { get; } + public int NewCapacity { get; } + + public InvalidUpdatedEventCapacityException(int currentCapacity, int newCapacity) + : base($"Invalid updated event capacity: {newCapacity}. It has to be greater than the current capacity: {currentCapacity}.") + { + CurrentCapacity = currentCapacity; + NewCapacity = newCapacity; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventFeeException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventFeeException.cs new file mode 100644 index 000000000..9a068f813 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidUpdatedEventFeeException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidUpdatedEventFeeException : AppException + { + public override string Code { get; } = "invalid_updated_event_fee"; + public decimal CurrentFee { get; } + public decimal NewFee { get; } + + public InvalidUpdatedEventFeeException(decimal currentFee, decimal newFee) + : base($"Updated fee: {newFee} cannot be greater than current fee: {currentFee}.") + { + CurrentFee = currentFee; + NewFee = newFee; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizationNotFoundException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizationNotFoundException.cs new file mode 100644 index 000000000..83e7bb79a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizationNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class OrganizationNotFoundException: AppException + { + public override string Code { get; } = "organization_not_found"; + public Guid OrganizationId { get; } + + public OrganizationNotFoundException(Guid organizationId): base($"Organization with id: '{organizationId}' was not found.") + { + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerDoesNotBelongToOrganizationException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerDoesNotBelongToOrganizationException.cs new file mode 100644 index 000000000..7e5388802 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/OrganizerDoesNotBelongToOrganizationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class OrganizerDoesNotBelongToOrganizationException : AppException + { + public override string Code => "organizer_does_not_belong_to_organization"; + public Guid OrganizerId { get; } + public Guid OrganizationId { get; } + + public OrganizerDoesNotBelongToOrganizationException(Guid organizerId, Guid organizationId) + : base($"Organizer with ID: {organizerId} does not belong to organization with ID: {organizationId}.") + { + OrganizerId = organizerId; + OrganizationId = organizationId; + } + } +} \ 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/UnauthorizedEventAccessException.cs similarity index 100% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedEventAccesException.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedEventAccessException.cs diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedOrganizerEventsAccessException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedOrganizerEventsAccessException.cs new file mode 100644 index 000000000..a542aadef --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/UnauthorizedOrganizerEventsAccessException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class UnauthorizedOrganizerEventsAccessException : AppException + { + public override string Code { get; } = "unauthorized_event_access"; + public Guid OrganizerId { get; } + public Guid UserId { get; } + + public UnauthorizedOrganizerEventsAccessException(Guid organizerId, Guid userId) + : base($"Unauthorized access to organizer events with ID: '{organizerId}' by user with ID: '{userId}'.") + { + OrganizerId = organizerId; + UserId = userId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventParticipants.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventParticipants.cs new file mode 100644 index 000000000..8101e4e4c --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetEventParticipants.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 GetEventParticipants : IQuery + { + public Guid EventId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs new file mode 100644 index 000000000..860aba8f2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Services.Clients +{ + public interface IFriendsServiceClient + { + Task> GetAsync(Guid studentId); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.cs new file mode 100644 index 000000000..49d592182 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IOrganizationsServiceClient.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 IOrganizationsServiceClient + { + 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 deleted file mode 100644 index ff8bc3d7a..000000000 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Events/EventService.cs +++ /dev/null @@ -1,50 +0,0 @@ -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/IEventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs index 5bde55989..7a3a9429c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs @@ -8,6 +8,7 @@ namespace MiniSpace.Services.Events.Application.Services { public interface IEventService { - Task>> SignInAsync(SearchEvents command); + Task>> BrowseEventsAsync(SearchEvents command); + Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents 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 index 1ed5a713a..cf1ad68b6 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs @@ -7,7 +7,16 @@ public interface IEventValidator { Category ParseCategory(string categoryString); DateTime ParseDate(string dateString, string fieldName); + State ParseState(string stateString); + EventEngagementType ParseEngagementType(string engagementTypeString); void ValidateDates(DateTime earlierDate, DateTime laterDate, string earlierDateString, string endDateString); (int pageNumber, int pageSize) PageFilter(int pageNumber, int pageSize); + void ValidateName(string name); + void ValidateDescription(string description); + void ValidateCapacity(int capacity); + void ValidateFee(decimal fee); + void ValidateUpdatedCapacity(int currentCapacity, int newCapacity); + void ValidateUpdatedFee(decimal currentFee, decimal newFee); + State? RestrictState(State? state); } } \ 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 index 6da0ed1dc..a00173b0f 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Address.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Address.cs @@ -14,5 +14,17 @@ public class Address( public string ApartmentNumber { get; set; } = apartmentNumber; public string City { get; set; } = city; public string ZipCode { get; set; } = zipCode; + + public Address Update(string buildingName, string street, string buildingNumber, string apartmentNumber, + string city, string zipCode) + { + BuildingName = buildingName == string.Empty ? BuildingName : buildingName; + Street = street == string.Empty ? Street : street; + BuildingNumber = buildingNumber == string.Empty ? BuildingNumber : buildingNumber; + ApartmentNumber = apartmentNumber == string.Empty ? ApartmentNumber : apartmentNumber; + City = city == string.Empty ? City : city; + ZipCode = zipCode == string.Empty ? ZipCode : zipCode; + return this; + } } } \ 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 index b9bf64a09..da9c0ffe0 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs @@ -8,7 +8,6 @@ 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(); @@ -24,12 +23,7 @@ public class Event: AggregateRoot 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 DateTime UpdatedAt { get; private set; } public IEnumerable InterestedStudents { @@ -51,7 +45,7 @@ public IEnumerable Ratings 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, + Organizer organizer, DateTime updatedAt, IEnumerable interestedStudents = null, IEnumerable signedUpStudents = null, IEnumerable ratings = null) { Id = id; @@ -65,32 +59,48 @@ public Event(AggregateId id, string name, string description, DateTime startDat Category = category; State = state; Organizer = organizer; - CoOrganizers = coOrganizers ?? Enumerable.Empty(); InterestedStudents = interestedStudents ?? Enumerable.Empty(); SignedUpStudents = signedUpStudents ?? Enumerable.Empty(); Ratings = ratings ?? Enumerable.Empty(); PublishDate = publishDate; + UpdatedAt = updatedAt; } 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) + Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, + Organizer organizer, DateTime now) { var @event = new Event(id, name, description, startDate, endDate, location, capacity, fee, category, - state, publishDate, organizer); + state, publishDate, organizer, now); return @event; } - public void AddOrganizer(Organizer organizer) + public void Update(string name, string description, DateTime startDate, DateTime endDate, Address location, + int capacity, decimal fee, Category category, State state, DateTime publishDate, DateTime now) + { + Name = name; + Description = description; + StartDate = startDate; + EndDate = endDate; + Location = location; + Capacity = capacity; + Fee = fee; + Category = category; + State = state; + PublishDate = publishDate; + UpdatedAt = now; + } + + public void SignUpStudent(Participant participant) { - if (CoOrganizers.Any(o => o.Id == organizer.Id)) + if(State != State.Published) { - throw new OrganizerAlreadyAddedException(organizer.Id); + throw new InvalidEventState(Id, State.Published, State); } - - _coOrganizers.Add(organizer); + AddParticipant(participant); } - public void SignUpStudent(Participant participant) + public void AddParticipant(Participant participant) { if (SignedUpStudents.Any(p => p.StudentId == participant.StudentId)) { @@ -101,10 +111,35 @@ public void SignUpStudent(Participant participant) { throw new EventCapacityExceededException(Id, Capacity); } + + if(participant.StudentId == Organizer.Id) + { + throw new OrganizerCannotSignUpForOwnEventException(Organizer.Id, Id); + } _signedUpStudents.Add(participant); } + public void CancelSignUp(Guid studentId) + { + if(State != State.Published) + { + throw new InvalidEventState(Id, State.Published, State); + } + RemoveParticipant(studentId); + } + + public void RemoveParticipant(Guid studentId) + { + var participant = _signedUpStudents.SingleOrDefault(p => p.StudentId == studentId); + if (participant is null) + { + throw new StudentNotSignedUpException(studentId, Id); + } + + _signedUpStudents.Remove(participant); + } + public void ShowStudentInterest(Participant participant) { if (InterestedStudents.Any(p => p.StudentId == participant.StudentId)) @@ -115,8 +150,24 @@ public void ShowStudentInterest(Participant participant) _interestedStudents.Add(participant); } + public void CancelInterest(Guid studentId) + { + var participant = _interestedStudents.SingleOrDefault(p => p.StudentId == studentId); + if (participant is null) + { + throw new StudentNotInterestedInEventException(studentId, Id); + } + + _interestedStudents.Remove(participant); + } + public void Rate(Guid studentId, int rating) { + if(State != State.Archived) + { + throw new InvalidEventState(Id, State.Archived, State); + } + if(_signedUpStudents.All(p => p.StudentId != studentId)) { throw new StudentNotSignedUpForEventException(Id ,studentId); @@ -148,6 +199,7 @@ public bool UpdateState(DateTime now) else return false; + UpdatedAt = now; return true; } @@ -160,5 +212,8 @@ private void ChangeState(State state) State = state; } + + public bool IsOrganizer(Guid organizerId) + => Organizer.Id == organizerId; } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventEngagementType.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventEngagementType.cs new file mode 100644 index 000000000..af5592f1d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventEngagementType.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Events.Core.Entities +{ + public enum EventEngagementType + { + SignedUp, + InterestedIn + } +} \ 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 index a2d988afa..1584b6870 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Organizer.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Organizer.cs @@ -7,14 +7,16 @@ public class Organizer public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } - public string Organization { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } - public Organizer(Guid id, string name, string email, string organization) + public Organizer(Guid id, string name, string email, Guid organizationId, string organizationName) { Id = id; Name = name; Email = email; - Organization = organization; + OrganizationId = organizationId; + OrganizationName = organizationName; } } } \ 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 index e80fdda37..7b0a03d91 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Participant.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Participant.cs @@ -2,10 +2,9 @@ namespace MiniSpace.Services.Events.Core.Entities { - public class Participant(Guid studentId, string name, string email) + public class Participant(Guid studentId, string name) { 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/Exceptions/InvalidEventState.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidEventState.cs new file mode 100644 index 000000000..8d5eaa711 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/InvalidEventState.cs @@ -0,0 +1,20 @@ +using System; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class InvalidEventState : DomainException + { + public override string Code { get; } = "invalid_event_state"; + public Guid EventId { get; } + public State RequiredState { get; } + public State CurrentState { get; } + public InvalidEventState(Guid eventId, State requiredState, State currentState) + : base($"Event with id: {eventId} has invalid state: {currentState}. Required state: {requiredState}") + { + EventId = eventId; + RequiredState = requiredState; + CurrentState = currentState; + } + } +} \ 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 deleted file mode 100644 index 13bed5f20..000000000 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerAlreadyAddedException.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/OrganizerCannotSignUpForOwnEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerCannotSignUpForOwnEventException.cs new file mode 100644 index 000000000..407b6eac4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/OrganizerCannotSignUpForOwnEventException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class OrganizerCannotSignUpForOwnEventException : DomainException + { + public override string Code { get; } = "organizer_cannot_sign_up_for_own_event"; + public Guid OrganizerId { get; } + public Guid EventId { get; } + public OrganizerCannotSignUpForOwnEventException(Guid organizerId, Guid eventId) + : base($"Organizer with id: {organizerId} cannot sign up for their own event with id: {eventId}.") + { + OrganizerId = organizerId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotInterestedInEventException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotInterestedInEventException.cs new file mode 100644 index 000000000..626d8269e --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotInterestedInEventException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentNotInterestedInEventException : DomainException + { + public override string Code { get; } = "student_is_not_interested_in_event"; + public Guid StudentId { get; } + public Guid EventId { get; } + + public StudentNotInterestedInEventException(Guid studentId, Guid eventId) + : base($"Student with ID: '{studentId}' is not 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/StudentNotSignedUpException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotSignedUpException.cs new file mode 100644 index 000000000..da58daf97 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/StudentNotSignedUpException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class StudentNotSignedUpException : DomainException + { + public override string Code { get; } = "student_not_signed_up"; + public Guid StudentId { get; } + public Guid EventId { get; } + + public StudentNotSignedUpException(Guid studentId, Guid eventId) + : base($"Student with ID: '{studentId}' has not 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/Repositories/IEventRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs index 41d28e0f3..bed362306 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs @@ -13,8 +13,12 @@ public interface IEventRepository 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); + Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseEventsAsync( + int pageNumber, int pageSize, string name, string organizer, DateTime dateFrom, DateTime dateTo, + Category? category, State? state, IEnumerable friends, EventEngagementType? friendsEngagementType, + IEnumerable sortBy, string direction, IEnumerable eventIds = null); + Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseOrganizerEventsAsync( + int pageNumber, int pageSize, string name, Guid organizerId, DateTime dateFrom, DateTime dateTo, + IEnumerable sortBy, string direction, State? state); } } \ 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 index 799ba2d49..28d680eea 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -1,5 +1,6 @@ using System; using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Events.Application.Commands; using MiniSpace.Services.Events.Application.Events.Rejected; using MiniSpace.Services.Events.Application.Exceptions; @@ -12,10 +13,66 @@ public object Map(Exception exception, object message) { // 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), + EventNotFoundException ex + => message switch + { + AddEventParticipant m => new AddEventParticipantRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + CancelInterestInEvent m => new CancelInterestInEventRejected(ex.EventId, ex.Message, ex.Code), + CancelSignUpToEvent m => new CancelSignUpToEventRejected(ex.EventId, ex.Message, ex.Code), + DeleteEvent m => new DeleteEventRejected(ex.EventId, ex.Message, ex.Code), + RateEvent m => new RateEventRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + RemoveEventParticipant m => new RemoveEventParticipantRejected(ex.EventId, ex.Message, ex.Code), + ShowInterestInEvent m => new ShowInterestInEventRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + SignUpToEvent m => new SignUpToEventRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + UpdateEvent m => new UpdateEventRejected(ex.EventId, ex.Message, ex.Code), + _ => null + }, + InvalidEventCategoryException ex + => message switch + { + AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + _ => null + }, + InvalidEventDateTimeException ex + => message switch + { + AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + _ => null + }, + InvalidEventDateTimeOrderException ex + => message switch + { + AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + _ => null + }, + OrganizerCannotAddEventForAnotherOrganizerException ex + => message switch + { + AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + _ => null + }, + StudentNotFoundException ex + => message switch + { + AddEventParticipant m => new AddEventParticipantRejected(m.EventId, ex.StudentId, ex.Message, ex.Code), + ShowInterestInEvent m => new ShowInterestInEventRejected(m.EventId, ex.StudentId, ex.Message, ex.Code), + SignUpToEvent m => new SignUpToEventRejected(m.EventId, ex.StudentId, ex.Message, ex.Code), + RateEvent m => new RateEventRejected(m.EventId, ex.StudentId, ex.Message, ex.Code), + _ => null + }, + UnauthorizedEventAccessException ex + => message switch + { + AddEventParticipant m => new AddEventParticipantRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + CancelInterestInEvent m => new CancelInterestInEventRejected(ex.EventId, ex.Message, ex.Code), + CancelSignUpToEvent m => new CancelSignUpToEventRejected(ex.EventId, ex.Message, ex.Code), + DeleteEvent m => new DeleteEventRejected(ex.EventId, ex.Message, ex.Code), + RemoveEventParticipant m => new RemoveEventParticipantRejected(ex.EventId, ex.Message, ex.Code), + ShowInterestInEvent m => new ShowInterestInEventRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + SignUpToEvent m => new SignUpToEventRejected(ex.EventId, m.StudentId, ex.Message, ex.Code), + UpdateEvent m => new UpdateEventRejected(ex.EventId, ex.Message, ex.Code), + _ => null + }, _ => null, }; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs index 82a642ba3..49a53898c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -37,7 +37,6 @@ 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; @@ -65,6 +64,8 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); @@ -104,9 +105,14 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseRabbitMq() .SubscribeCommand() .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() .SubscribeEvent(); return app; 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 index 66a7e31af..f9dec514b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -62,6 +62,20 @@ private static IReadOnlyDictionary MessageTemplates After = "Events' state updated at: {Now}." } }, + { + typeof(AddEventParticipant), + new HandlerLogTemplate + { + After = "Added a participant with id: {StudentId} to event with id: {EventId}.", + } + }, + { + typeof(RemoveEventParticipant), + new HandlerLogTemplate + { + After = "Removed participant with id: {ParticipantId} from event with id: {EventId}." + } + }, { typeof(StudentCreated), new HandlerLogTemplate 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 index 9c8c88738..dffb5c2f7 100644 --- 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 @@ -14,7 +14,6 @@ public class EventDocument : IIdentifiable 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; } @@ -24,6 +23,7 @@ public class EventDocument : IIdentifiable public Category Category { get; set; } public State State { get; set; } public DateTime PublishDate { get; set; } + public DateTime UpdatedAt { 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 index 271978669..5464f8410 100644 --- 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 @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Core.Entities; @@ -6,7 +8,7 @@ namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents { public static class Extensions { - public static EventDto AsDto(this EventDocument document) + public static EventDto AsDto(this EventDocument document, Guid studentId) => new () { Id = document.Id, @@ -15,7 +17,6 @@ public static EventDto AsDto(this EventDocument document) 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(), @@ -23,13 +24,29 @@ public static EventDto AsDto(this EventDocument document) Fee = document.Fee, Category = document.Category.ToString(), Status = document.State.ToString(), - PublishDate = document.PublishDate + PublishDate = document.PublishDate, + UpdatedAt = document.UpdatedAt, + IsSignedUp = document.SignedUpStudents.Any(x => x.StudentId == studentId), + IsInterested = document.InterestedStudents.Any(x => x.StudentId == studentId), + HasRated = document.Ratings.Any(x => x.StudentId == studentId) }; + + public static EventDto AsDtoWithFriends(this EventDocument document, Guid studentId, IEnumerable friends) + { + var eventDto = document.AsDto(studentId); + eventDto.FriendsInterestedIn = document.InterestedStudents + .Where(x => friends.Any(f => f.FriendId == x.StudentId)) + .Select(p => p.AsDto()); + eventDto.FriendsSignedUp = document.SignedUpStudents + .Where(x => friends.Any(f => f.FriendId == x.StudentId)) + .Select(p => p.AsDto()); + return eventDto; + } 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); + document.Organizer, document.UpdatedAt,document.InterestedStudents, document.SignedUpStudents, document.Ratings); public static EventDocument AsDocument(this Event entity) => new () @@ -41,7 +58,6 @@ public static EventDocument AsDocument(this Event entity) StartDate = entity.StartDate, EndDate = entity.EndDate, Location = entity.Location, - CoOrganizers = entity.CoOrganizers, InterestedStudents = entity.InterestedStudents, SignedUpStudents = entity.SignedUpStudents, Capacity = entity.Capacity, @@ -49,8 +65,17 @@ public static EventDocument AsDocument(this Event entity) Category = entity.Category, State = entity.State, PublishDate = entity.PublishDate, + UpdatedAt = entity.UpdatedAt, Ratings = entity.Ratings }; + + public static EventParticipantsDto AsDto(this EventDocument document) + => new () + { + EventId = document.Id, + InterestedStudents = document.InterestedStudents.Select(p => p.AsDto()), + SignedUpStudents = document.SignedUpStudents.Select(p => p.AsDto()) + }; public static AddressDto AsDto(this Address entity) => new () @@ -72,7 +97,8 @@ public static OrganizerDto AsDto(this Organizer entity) Id = entity.Id, Name = entity.Name, Email = entity.Email, - Organization = entity.Organization + OrganizationId = entity.OrganizationId, + OrganizationName = entity.OrganizationName }; public static StudentDocument AsDocument(this Student entity) @@ -83,5 +109,12 @@ public static StudentDocument AsDocument(this Student entity) public static Student AsEntity(this StudentDocument document) => new (document.Id); + + public static ParticipantDto AsDto(this Participant entity) + => new () + { + StudentId = entity.StudentId, + Name = entity.Name + }; } } \ 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 index e840928e4..25e976803 100644 --- 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 @@ -1,12 +1,16 @@ using System; +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.Events; using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; +using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers @@ -14,20 +18,35 @@ namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers public class GetEventHandler : IQueryHandler { private readonly IMongoRepository _eventRepository; + private readonly IFriendsServiceClient _friendsServiceClient; + private readonly IAppContext _appContext; private readonly IMessageBroker _messageBroker; - public GetEventHandler(IMongoRepository eventRepository, IMessageBroker messageBroker) + public GetEventHandler(IMongoRepository eventRepository, + IFriendsServiceClient friendsServiceClient, IAppContext appContext, IMessageBroker messageBroker) { _eventRepository = eventRepository; + _friendsServiceClient = friendsServiceClient; + _appContext = appContext; _messageBroker = messageBroker; } public async Task HandleAsync(GetEvent query, CancellationToken cancellationToken) { var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); + if(document is null) + { + return null; + } + var identity = _appContext.Identity; + var friends = Enumerable.Empty(); + if(identity.IsAuthenticated) + { + friends = await _friendsServiceClient.GetAsync(identity.Id); + } await _messageBroker.PublishAsync(new EventViewed(query.EventId)); - return document?.AsDto(); + return document.AsDtoWithFriends(identity.Id, friends); } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventParticipantsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventParticipantsHandler.cs new file mode 100644 index 000000000..2fd84959a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventParticipantsHandler.cs @@ -0,0 +1,44 @@ +using System; +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.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 GetEventParticipantsHandler : IQueryHandler + { + private readonly IMongoRepository _eventRepository; + private readonly IAppContext _appContext; + + public GetEventParticipantsHandler(IMongoRepository eventRepository, + IAppContext appContext) + { + _eventRepository = eventRepository; + _appContext = appContext; + } + + public async Task HandleAsync(GetEventParticipants query, CancellationToken cancellationToken) + { + var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); + if(document is null) + { + return null; + } + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != document.Organizer.Id && !identity.IsAdmin) + { + return null; + } + + 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 index 9d10756e6..14818f556 100644 --- 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 @@ -43,11 +43,11 @@ public async Task>> HandleAsync(GetStudentEv var studentEvents = await _studentsServiceClient.GetAsync(query.StudentId); var studentEventIds = studentEvents.InterestedInEvents.Union(studentEvents.SignedUpEvents).ToList(); - var result = await _eventRepository.BrowseAsync(1, query.NumberOfResults, - string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue, - Enumerable.Empty(), "asc", State.Published, studentEventIds); + var result = await _eventRepository.BrowseEventsAsync(1, query.NumberOfResults, + string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue, null, null, + Enumerable.Empty(), null, Enumerable.Empty(), "asc", studentEventIds); - return new PagedResponse>(result.Item1.Select(e => new EventDto(e)), + return new PagedResponse>(result.Item1.Select(e => new EventDto(e, identity.Id)), result.Item2, result.Item3, result.Item4, result.Item5);; } } 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 index 2fda11c8e..af2c686d7 100644 --- 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 @@ -37,21 +37,50 @@ public async Task> GetAllAsync() 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) + private async Task<(int totalPages, int totalElements, IEnumerable data)> BrowseAsync( + FilterDefinition filterDefinition, SortDefinition sortDefinition, + int pageNumber, int pageSize) { - 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 pagedEvents; + } + + public async Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseEventsAsync( + int pageNumber, int pageSize, string name, string organizer, DateTime dateFrom, DateTime dateTo, + Category? category, State? state, IEnumerable friends, EventEngagementType? friendsEngagementType, + IEnumerable sortBy, string direction, IEnumerable eventIds = null) + { + var filterDefinition = Extensions.ToFilterDefinition(name, dateFrom, dateTo, eventIds) + .AddOrganizerNameFilter(organizer) + .AddCategoryFilter(category) + .AddRestrictedStateFilter(state) + .AddFriendsFilter(friends, friendsEngagementType); + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); + + return (pagedEvents.data.Select(e => e.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); + } + + public async Task<(IEnumerable events, int pageNumber, int pageSize, int totalPages, int totalElements)> BrowseOrganizerEventsAsync(int pageNumber, + int pageSize, string name, Guid organizerId, DateTime dateFrom, DateTime dateTo, IEnumerable sortBy, + string direction, State? state) + { + var filterDefinition = Extensions.ToFilterDefinition(name, dateFrom, dateTo) + .AddOrganizerIdFilter(organizerId) + .AddStateFilter(state); + var sortDefinition = Extensions.ToSortDefinition(sortBy, direction); + + var pagedEvents = await BrowseAsync(filterDefinition, sortDefinition, pageNumber, pageSize); - return new Tuple,int,int,int,int>(pagedEvents.data.Select(e => e.AsEntity()), - pageNumber, pageSize, pagedEvents.totalPages, pagedEvents.totalElements); + return (pagedEvents.data.Select(e => e.AsEntity()), pageNumber, pageSize, + pagedEvents.totalPages, pagedEvents.totalElements); } public Task AddAsync(Event @event) => _repository.AddAsync(@event.AsDocument()); 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 index 17ec0605f..1c5d00f6b 100644 --- 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 @@ -3,7 +3,6 @@ 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; @@ -13,6 +12,7 @@ namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories { public static class Extensions { + private static readonly FilterDefinitionBuilder FilterDefinitionBuilder = Builders.Filter; public static async Task<(int totalPages, int totalElements, IReadOnlyList data)> AggregateByPage( this IMongoCollection collection, FilterDefinition filterDefinition, @@ -46,7 +46,11 @@ public static class Extensions ?.FirstOrDefault() ?.Count; - var totalPages = (int)Math.Ceiling((double)count/ pageSize); + if (count == null) + { + return (0, 0, Array.Empty()); + } + var totalPages = (int)Math.Ceiling((double)count / pageSize); var data = aggregation.First() .Facets.First(x => x.Name == "data") @@ -55,38 +59,107 @@ public static class Extensions return (totalPages, (int)count, data); } - public static FilterDefinition ToFilterDefinition(string name, string organizer, - DateTime dateFrom, DateTime dateTo, State state, IEnumerable eventIds = null) + public static FilterDefinition ToFilterDefinition(string name, DateTime dateFrom, + DateTime dateTo, IEnumerable eventIds = null) { - var filterDefinitionBuilder = Builders.Filter; - var filterDefinition = filterDefinitionBuilder.Empty; - filterDefinition &= filterDefinitionBuilder.Eq(x => x.State, state); + var filterDefinition = FilterDefinitionBuilder.Empty; if (!string.IsNullOrWhiteSpace(name)) { - filterDefinition &= filterDefinitionBuilder.Regex(x => x.Name, + filterDefinition &= FilterDefinitionBuilder.Regex(x => x.Name, new BsonRegularExpression(new Regex($".*{name}.*", 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 FilterDefinition AddOrganizerNameFilter ( + this FilterDefinition filterDefinition, string organizer) + { if (!string.IsNullOrWhiteSpace(organizer)) { - filterDefinition &= filterDefinitionBuilder.Regex(x => x.Organizer.Name, + filterDefinition &= FilterDefinitionBuilder.Regex(x => x.Organizer.OrganizationName, new BsonRegularExpression(new Regex($".*{organizer}.*", RegexOptions.IgnoreCase))); } - if (dateFrom != DateTime.MinValue) + return filterDefinition; + } + + public static FilterDefinition AddOrganizerIdFilter (this FilterDefinition filterDefinition, Guid organizerId) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.Organizer.Id, organizerId); + return filterDefinition; + } + + public static FilterDefinition AddCategoryFilter (this FilterDefinition filterDefinition, Category? category) + { + if (category != null) { - filterDefinition &= filterDefinitionBuilder.Gte(x => x.StartDate, dateFrom); + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.Category, category); } - if (dateTo != DateTime.MinValue) + return filterDefinition; + } + + public static FilterDefinition AddStateFilter (this FilterDefinition filterDefinition, State? state) + { + if (state != null) { - filterDefinition &= filterDefinitionBuilder.Lte(x => x.EndDate, dateTo); + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.State, state); } - if (eventIds != null) + return filterDefinition; + } + + public static FilterDefinition AddRestrictedStateFilter (this FilterDefinition filterDefinition, State? state) + { + if (state != null) + { + filterDefinition &= FilterDefinitionBuilder.Eq(x => x.State, state); + } + else + { + filterDefinition &= FilterDefinitionBuilder.In(x => x.State, new[] { State.Published, State.Archived }); + } + + return filterDefinition; + } + + public static FilterDefinition AddFriendsFilter (this FilterDefinition filterDefinition, + IEnumerable friendsEnumerable, EventEngagementType? friendsEngagementType) + { + var friends = friendsEnumerable.ToList(); + if (friends.Count == 0) + { + return filterDefinition; + } + + if (friendsEngagementType != null) + { + filterDefinition &= friendsEngagementType == EventEngagementType.InterestedIn + ? FilterDefinitionBuilder.ElemMatch(x => x.InterestedStudents, s => friends.Contains(s.StudentId)) + : FilterDefinitionBuilder.ElemMatch(x => x.SignedUpStudents, s => friends.Contains(s.StudentId)); + } + else { - filterDefinition &= filterDefinitionBuilder.In(x => x.Id, eventIds); + var interestedFilter = FilterDefinitionBuilder.ElemMatch(x => x.InterestedStudents, s => friends.Contains(s.StudentId)); + var signedUpFilter = FilterDefinitionBuilder.ElemMatch(x => x.SignedUpStudents, s => friends.Contains(s.StudentId)); + filterDefinition &= FilterDefinitionBuilder.Or(interestedFilter, signedUpFilter); } return filterDefinition; @@ -95,16 +168,17 @@ public static FilterDefinition ToFilterDefinition(string name, st public static SortDefinition ToSortDefinition(IEnumerable sortByArguments, string direction) { var sort = sortByArguments.ToList(); - if(!sort.Any()) + if(sort.Count == 0) { - sort.Add("PublishDate"); + sort.Add("StartDate"); } var sortDefinitionBuilder = Builders.Sort; + var sortStateDefinition = new[] { sortDefinitionBuilder.Descending("State") }; var sortDefinition = sort .Select(sortBy => direction == "asc" ? sortDefinitionBuilder.Ascending(sortBy) : sortDefinitionBuilder.Descending(sortBy)); - var sortCombined = sortDefinitionBuilder.Combine(sortDefinition); + var sortCombined = sortDefinitionBuilder.Combine(sortStateDefinition.Concat(sortDefinition)); return sortCombined; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs new file mode 100644 index 000000000..2e3e7f1ba --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +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 FriendsServiceClient : IFriendsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public FriendsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["friends"]; + } + + public Task> GetAsync(Guid studentId) + => _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.cs new file mode 100644 index 000000000..60d490d8f --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/OrganizationsServiceClient.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 OrganizationsServiceClient : IOrganizationsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public OrganizationsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["organizations"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/organizations/{id}/details"); + + } +} \ 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 index 38fe02a95..b96602bf5 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventMapper.cs @@ -14,10 +14,10 @@ public IEnumerable MapAll(IEnumerable events) public IEvent Map(IDomainEvent @event) { // TODO: update mapper - // switch (@event) - // { - // - // } + switch (@event) + { + + } return null; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs new file mode 100644 index 000000000..05aff43b4 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Application.Commands; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Exceptions; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Infrastructure.Services +{ + public class EventService : IEventService + { + private readonly IEventRepository _eventRepository; + private readonly IEventValidator _eventValidator; + private readonly IAppContext _appContext; + + public EventService(IEventRepository eventRepository, IEventValidator eventValidator, IAppContext appContext) + { + _eventRepository = eventRepository; + _eventValidator = eventValidator; + _appContext = appContext; + } + + public async Task>> BrowseEventsAsync(SearchEvents command) + { + var dateFrom = DateTime.MinValue; + var dateTo = DateTime.MinValue; + Category? category = null; + State? state = null; + EventEngagementType? friendsEngagementType = null; + if(command.DateFrom != string.Empty) + { + dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + } + if(command.DateTo != string.Empty) + { + dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); + } + if(command.Category != string.Empty) + { + category = _eventValidator.ParseCategory(command.Category); + } + if(command.State != string.Empty) + { + state = _eventValidator.ParseState(command.State); + state = _eventValidator.RestrictState(state); + } + if(command.FriendsEngagementType != string.Empty) + { + friendsEngagementType = _eventValidator.ParseEngagementType(command.FriendsEngagementType); + } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); + + var result = await _eventRepository.BrowseEventsAsync( + pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, category, state, command.Friends, + friendsEngagementType, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); + + var identity = _appContext.Identity; + var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, identity.Id)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + return pagedEvents; + } + + public async Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents command) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != command.OrganizerId && !identity.IsAdmin) + { + throw new UnauthorizedOrganizerEventsAccessException(command.OrganizerId, identity.Id); + } + var dateFrom = DateTime.MinValue; + var dateTo = DateTime.MinValue; + State? state = null; + if(command.DateFrom != string.Empty) + { + dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + } + if(command.DateTo != string.Empty) + { + dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); + } + if(command.State != string.Empty) + { + state = _eventValidator.ParseState(command.State); + } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); + + var result = await _eventRepository.BrowseOrganizerEventsAsync( + pageNumber, pageSize, command.Name, command.OrganizerId, dateFrom, dateTo, + command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction, state); + + var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, _appContext.Identity.Id)), + result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + return pagedEvents; + } + } +} \ 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 index 66f36cd55..780893f12 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs @@ -28,6 +28,24 @@ public DateTime ParseDate(string dateString, string fieldName) } return date; } + + public State ParseState(string stateString) + { + if (!Enum.TryParse(stateString, true, out var state)) + { + throw new InvalidEventStateException(stateString); + } + return state; + } + + public EventEngagementType ParseEngagementType(string engagementTypeString) + { + if (!Enum.TryParse(engagementTypeString, true, out var engagementType)) + { + throw new InvalidEventEngagementTypeException(engagementTypeString); + } + return engagementType; + } public void ValidateDates(DateTime earlierDate, DateTime laterDate, string earlierDateField, string laterDateField) { @@ -43,6 +61,47 @@ public void ValidateDates(DateTime earlierDate, DateTime laterDate, string earl return (pageNumber, pageSize); } - // TODO: Add Address validation + public void ValidateName(string name) + { + if (string.IsNullOrWhiteSpace(name) || name.Length > 300) + throw new InvalidEventNameException(name); + } + + public void ValidateDescription(string description) + { + if (string.IsNullOrWhiteSpace(description) || description.Length > 1000) + throw new InvalidEventDescriptionException(description); + } + + public void ValidateCapacity(int capacity) + { + if (capacity <= 0 || capacity > 1000) + throw new InvalidEventCapacityException(capacity); + } + + public void ValidateFee(decimal fee) + { + if (fee < 0.0m || fee > 1000.0m) + throw new InvalidEventFeeException(fee); + } + + public void ValidateUpdatedCapacity(int currentCapacity, int newCapacity) + { + if (newCapacity < currentCapacity) + throw new InvalidUpdatedEventCapacityException(currentCapacity, newCapacity); + } + + public void ValidateUpdatedFee(decimal currentFee, decimal newFee) + { + if (newFee > currentFee) + throw new InvalidUpdatedEventFeeException(currentFee, newFee); + } + + public State? RestrictState(State? state) + { + if (state != State.Published && state != State.Archived) + return null; + return state; + } } } \ 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 index 0083c2815..32137f273 100644 --- 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 @@ -16,7 +16,7 @@ public class EventStateUpdaterWorker: BackgroundService private readonly IMessageBroker _messageBroker; private readonly ICommandDispatcher _commandDispatcher; private readonly IDateTimeProvider _dateTimeProvider; - private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(10); + private const int MinutesInterval = 5; public EventStateUpdaterWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, IDateTimeProvider dateTimeProvider) @@ -33,8 +33,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - await _commandDispatcher.SendAsync(new UpdateEventsState(_dateTimeProvider.Now), stoppingToken); - await Task.Delay(_updateInterval, stoppingToken); + var now = _dateTimeProvider.Now; + var minutes = now.Minute; + if (minutes % MinutesInterval == 0) + { + await _commandDispatcher.SendAsync(new UpdateEventsState(now), stoppingToken); + } + + var nextTime = now.AddMinutes(MinutesInterval - (minutes % MinutesInterval)).AddSeconds(-now.Second) + .AddMilliseconds(-now.Millisecond); + var delay = nextTime - now; + + await Task.Delay(delay, stoppingToken); } catch (TaskCanceledException) { diff --git a/MiniSpace.Services.Friends/.gitignore b/MiniSpace.Services.Friends/.gitignore new file mode 100644 index 000000000..64def56a6 --- /dev/null +++ b/MiniSpace.Services.Friends/.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.Friends/Dockerfile b/MiniSpace.Services.Friends/Dockerfile new file mode 100644 index 000000000..a680debc4 --- /dev/null +++ b/MiniSpace.Services.Friends/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Friends.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.Friends.Api.dll"] \ No newline at end of file diff --git a/MiniSpace.Services.Friends/LICENSE b/MiniSpace.Services.Friends/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.Friends/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.Friends/MiniSpace.Services.Friends.sln b/MiniSpace.Services.Friends/MiniSpace.Services.Friends.sln new file mode 100644 index 000000000..ff5faab9b --- /dev/null +++ b/MiniSpace.Services.Friends/MiniSpace.Services.Friends.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.Friends/scripts/build.sh b/MiniSpace.Services.Friends/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Friends/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Friends/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Friends/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..017dc287b --- /dev/null +++ b/MiniSpace.Services.Friends/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.friends:latest . + +docker tag minispace.services.friends:latest adrianvsaint/minispace.services.friends:latest + +docker push adrianvsaint/minispace.services.friends:latest diff --git a/MiniSpace.Services.Friends/scripts/start.sh b/MiniSpace.Services.Friends/scripts/start.sh new file mode 100644 index 000000000..29eeea1f8 --- /dev/null +++ b/MiniSpace.Services.Friends/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.Friends/scripts/test.sh b/MiniSpace.Services.Friends/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Friends/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.Api.csproj b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.Api.csproj new file mode 100644 index 000000000..d3ab4d661 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Friends.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.Api.sln b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.Api.sln new file mode 100644 index 000000000..ae866df97 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/MiniSpace.Services.Friends.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.Friends.Api", "MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Api/Program.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs new file mode 100644 index 000000000..c0456a0ca --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Queries; +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.Friends.Application; +using MiniSpace.Services.Friends.Application.Commands; +using MiniSpace.Services.Friends.Application.Dto; +// using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Infrastructure; + +namespace MiniSpace.Services.Friends.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>("friends/{studentId}", + // ctx => new GetFriends { StudentId = Guid.Parse(ctx.Request.RouteValues["studentId"].ToString()) }, + // (query, ctx) => ctx.Response.WriteAsJsonAsync(query), // Correctly define delegate with parameters + // afterDispatch: ctx => ctx.Response.Ok()) + .Get>("friends/{studentId}") + .Get>("friends/requests/{studentId}") + .Get>("friends/pending") + .Get>("friends/pending/all") + .Get>("friends/requests/sent/{studentId}") + // .Get("friends/requests/sent", ctx => + // { + // var query = new GetSentFriendRequests { StudentId = ctx.User.GetUserId() }; + // return ctx.QueryDispatcher.QueryAsync(query); + // }, afterDispatch: ctx => ctx.Response.WriteAsJsonAsync(ctx.Result)) + + .Post("friends/requests/{studentId}/accept", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("friends/requests/{studentId}/decline", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Delete("friends/{requesterId}/{friendId}/remove") + .Post("friends/{studentId}/invite", afterDispatch: (cmd, ctx) => ctx.Response.Created($"friends/{ctx.Request.RouteValues["studentId"]}/invite")))) + .UseLogging() + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Properties/launchSettings.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Properties/launchSettings.json new file mode 100644 index 000000000..1f24baab2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5012" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Friends": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5012", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.Development.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.Development.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.docker.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.docker.json new file mode 100644 index 000000000..c7850c7fb --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Friends Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "friends-service", + "address": "friends-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "friends-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": "friends", + "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": "friends-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "friends-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": "friends" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "friends-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "friends:" + }, + "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.Friends/src/MiniSpace.Services.Friends.Api/appsettings.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.local.json b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.local.json new file mode 100644 index 000000000..65ea837fc --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Friends Service", + "service": "friends-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "friends-service", + "address": "docker.for.win.localhost", + "port": "5009", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "friends-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": "friends", + "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": "friends-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "friends-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": "friends" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "friends-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "friends:" + }, + "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": "friends-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "friends-service", + "commonName": "friends-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "friends-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs new file mode 100644 index 000000000..dc0491a11 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class AddFriend : ICommand + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public AddFriend(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs new file mode 100644 index 000000000..90e96e2e3 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/FriendRequestSent.cs @@ -0,0 +1,18 @@ + +// using Convey.CQRS.Commands; +// using Convey.CQRS.Events; + +// namespace MiniSpace.Services.Friends.Application.Events +// { +// public class FriendRequestSent : IEvent +// { +// public Guid InviterId { get; } +// public Guid InviteeId { get; } + +// public FriendRequestSent(Guid inviterId, Guid inviteeId) +// { +// InviterId = inviterId; +// InviteeId = inviteeId; +// } +// } +// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs new file mode 100644 index 000000000..470efda1f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Application.Services; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class AddFriendHandler : ICommandHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + + public AddFriendHandler(IFriendRepository friendRepository, IAppContext appContext, + IMessageBroker messageBroker, IEventMapper eventMapper) + { + _friendRepository = friendRepository; + _appContext = appContext; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + } + + public async Task HandleAsync(AddFriend command, CancellationToken cancellationToken = default) + { + if (!ValidateAccessOrFail(command.RequesterId)) + { + throw new UnauthorizedFriendActionException(command.RequesterId, command.FriendId); + } + + + var alreadyFriends = await _friendRepository.IsFriendAsync(command.RequesterId, command.FriendId); + if (alreadyFriends) + { + throw new AlreadyFriendsException(command.RequesterId, command.FriendId); + } + + var requester = await _friendRepository.GetFriendshipAsync(command.RequesterId, command.FriendId); + if (requester == null) + { + throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); + } + await _friendRepository.UpdateFriendshipAsync(requester); + var events = _eventMapper.MapAll(requester.Events); + await _messageBroker.PublishAsync(events); + } + + private bool ValidateAccessOrFail(Guid requesterId) + { + var identity = _appContext.Identity; + return identity.IsAuthenticated && identity.Id == requesterId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs new file mode 100644 index 000000000..4fedfe525 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class FriendAddedHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendAddedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(FriendAdded @event, CancellationToken cancellationToken) + { + var friendship = await _friendRepository.GetFriendshipAsync(@event.RequesterId, @event.FriendId); + if (friendship is null) + { + throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); + } + + friendship.MarkAsConfirmed(); + await _friendRepository.UpdateAsync(friendship); + + var events = _eventMapper.MapAll(friendship.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs new file mode 100644 index 000000000..f48ac5086 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendRequestSentHandler.cs @@ -0,0 +1,40 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class FriendRequestSentHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public FriendRequestSentHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var request = new FriendRequest( + inviterId: @event.InviterId, + inviteeId: @event.InviteeId, + requestedAt: now, + state: FriendState.Requested + ); + await _friendRepository.AddRequestAsync(request); + + var events = _eventMapper.MapAll(request.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs new file mode 100644 index 000000000..492f7a487 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs @@ -0,0 +1,57 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class InviteFriendHandler : ICommandHandler + { + private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + private readonly IAppContext _appContext; + + public InviteFriendHandler(IFriendRequestRepository friendRequestRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) + { + _friendRequestRepository = friendRequestRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + _appContext = appContext; + } + + public async Task HandleAsync(InviteFriend command, CancellationToken cancellationToken = default) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated || identity.Id == command.InviterId) + { + // TODO:: UPDATE THE LOGIC HERE FOR THE PRODUCTION.❗ + // throw new UnauthorizedFriendActionException(command.InviterId, command.InviteeId); + } + + var existingRequest = await _friendRequestRepository.FindByInviterAndInvitee(command.InviterId, command.InviteeId); + + if (existingRequest != null && existingRequest.State == FriendState.Requested) + { + throw new AlreadyInvitedException(command.InviterId, command.InviteeId); + } + + var friendRequest = new FriendRequest( + inviterId: command.InviterId, + inviteeId: command.InviteeId, + requestedAt: DateTime.UtcNow, + state: FriendState.Requested + ); + + await _friendRequestRepository.AddAsync(friendRequest); + + // Optionally, publish an event about the friend request + var friendInvitedEvent = new FriendInvited(command.InviterId, command.InviteeId); + await _messageBroker.PublishAsync(friendInvitedEvent); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs new file mode 100644 index 000000000..be2390957 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs @@ -0,0 +1,57 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class PendingFriendAcceptHandler : ICommandHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + + public PendingFriendAcceptHandler( + IFriendRequestRepository friendRequestRepository, + IFriendRepository friendRepository, + IMessageBroker messageBroker, + IEventMapper eventMapper) + + { + _friendRequestRepository = friendRequestRepository; + _friendRepository = friendRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + } + + public async Task HandleAsync(PendingFriendAccept command, CancellationToken cancellationToken = default) + { + // Fetch the friend request to confirm it exists and is valid + var friendRequest = await _friendRequestRepository.FindByInviterAndInvitee(command.RequesterId, command.FriendId); + if (friendRequest == null) + { + throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); + } + + if (friendRequest.State != FriendState.Requested) + { + throw new InvalidOperationException("Friend request is not in the correct state to be accepted."); + } + + // Accept the friend request + friendRequest.Accept(); + await _friendRequestRepository.UpdateAsync(friendRequest); + + // Create a new friend relationship + var newFriend = new Friend(command.RequesterId, command.FriendId, DateTime.UtcNow, FriendState.Accepted); + await _friendRepository.AddAsync(newFriend); + + // Optionally, create the reciprocal friendship to reflect the two-way relationship + var reciprocalFriend = new Friend(command.FriendId, command.RequesterId, DateTime.UtcNow, FriendState.Accepted); + await _friendRepository.AddAsync(reciprocalFriend); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs new file mode 100644 index 000000000..1be3e5263 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs @@ -0,0 +1,47 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class PendingFriendDeclineHandler : ICommandHandler + { + private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + + public PendingFriendDeclineHandler( + IFriendRequestRepository friendRequestRepository, + IMessageBroker messageBroker, + IEventMapper eventMapper) + { + _friendRequestRepository = friendRequestRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + } + + public async Task HandleAsync(PendingFriendDecline command, CancellationToken cancellationToken = default) + { + var friendRequest = await _friendRequestRepository.FindByInviterAndInvitee(command.RequesterId, command.FriendId); + if (friendRequest == null) + { + throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); + } + + if (friendRequest.State != Core.Entities.FriendState.Requested) + { + throw new InvalidOperationException("Friend request is not in the correct state to be declined."); + } + + friendRequest.Decline(); + friendRequest.State = Core.Entities.FriendState.Declined; + await _friendRequestRepository.UpdateAsync(friendRequest); + + // var events = _eventMapper.MapAll(friendRequest.Events); + // await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs new file mode 100644 index 000000000..cdccb911b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs @@ -0,0 +1,54 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Commands.Handlers +{ + public class RemoveFriendHandler : ICommandHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IMessageBroker _messageBroker; + private readonly IEventMapper _eventMapper; + private readonly IAppContext _appContext; + + public RemoveFriendHandler(IFriendRepository friendRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) + { + _friendRepository = friendRepository; + _messageBroker = messageBroker; + _eventMapper = eventMapper; + _appContext = appContext; + } + + public async Task HandleAsync(RemoveFriend command, CancellationToken cancellationToken = default) + { + var identity = _appContext.Identity; + // if (!identity.IsAuthenticated) + // { + // throw new UnauthorizedFriendActionException(command.RequesterId, identity.Id); + // } + Console.WriteLine($"Handling RemoveFriend for RequesterId: {command.RequesterId} and FriendId: {command.FriendId}. Authenticated: {identity.IsAuthenticated}"); + + + var exists = await _friendRepository.IsFriendAsync(command.RequesterId, command.FriendId); + if (!exists) + { + throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); + } + + // Remove the friendship in both directions + await _friendRepository.RemoveFriendAsync(command.RequesterId, command.FriendId); + await _friendRepository.RemoveFriendAsync(command.FriendId, command.RequesterId); + + // Publish an event indicating the friend has been removed + var eventToPublish = new PendingFriendDeclined(command.RequesterId, command.FriendId); + await _messageBroker.PublishAsync(eventToPublish); + + // Publish a reciprocal event for the inverse relationship + var reciprocalEventToPublish = new PendingFriendDeclined(command.FriendId, command.RequesterId); + await _messageBroker.PublishAsync(reciprocalEventToPublish); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/InviteFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/InviteFriend.cs new file mode 100644 index 000000000..24c2723c2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/InviteFriend.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class InviteFriend : ICommand + { + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + + public InviteFriend(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendAccept.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendAccept.cs new file mode 100644 index 000000000..428b3f368 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendAccept.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class PendingFriendAccept : ICommand + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendAccept(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendDecline.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendDecline.cs new file mode 100644 index 000000000..1d16eb063 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/PendingFriendDecline.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class PendingFriendDecline : ICommand + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendDecline(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/RemoveFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/RemoveFriend.cs new file mode 100644 index 000000000..0ca655626 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/RemoveFriend.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Commands +{ + public class RemoveFriend : ICommand + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public RemoveFriend(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs new file mode 100644 index 000000000..3313d86bc --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class StudentCreated : ICommand + { + public Guid StudentId { get; } + public string Email { get; } + + public StudentCreated(Guid studentId, string email) + { + StudentId = studentId; + Email = email; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs new file mode 100644 index 000000000..c0962d17f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class StudentDeleted : ICommand + { + public Guid StudentId { get; } + + public StudentDeleted(Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/ContractAttribute.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/ContractAttribute.cs new file mode 100644 index 000000000..dae70f4b3 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Friends.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs new file mode 100644 index 000000000..59502bf48 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs @@ -0,0 +1,17 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class FriendDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public Guid StudentId { get; set; } + public Guid FriendId { get; set; } + public string LastName { get; set; } + public string FullName => $"{FirstName} {LastName}"; + public DateTime CreatedAt { get; set; } + public FriendState State { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendEventsDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendEventsDto.cs new file mode 100644 index 000000000..78ce1aefa --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendEventsDto.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class FriendEventsDto + { + public Guid FriendId { get; set; } + public IEnumerable CommonEvents { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs new file mode 100644 index 000000000..6af0e7b5f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class FriendRequestDto + { + public Guid Id { get; set; } + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + public DateTime RequestedAt { get; set; } + public FriendState State { get; set; } + public Guid StudentId { get; set; } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs new file mode 100644 index 000000000..d99a8603b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class FriendAdded : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendAdded(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendInvited.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendInvited.cs new file mode 100644 index 000000000..959d290e1 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendInvited.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class FriendInvited : IEvent, IDomainEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendInvited(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestCreated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestCreated.cs new file mode 100644 index 000000000..31fbdcbca --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class FriendRequestCreated : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendRequestCreated(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestSent.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestSent.cs new file mode 100644 index 000000000..e486c6f25 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestSent.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class FriendRequestSent : IEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public FriendRequestSent(Guid inviterId, Guid inviteeId) + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs new file mode 100644 index 000000000..c3220cd56 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs @@ -0,0 +1,39 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Events.External.Handlers +{ + public class FriendInvitedHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendInvitedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(FriendInvited @event, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var request = new FriendRequest( + inviterId: @event.InviterId, + inviteeId: @event.InviteeId, + requestedAt: now, + state: FriendState.Requested + ); + + await _friendRepository.AddInvitationAsync(request); + + var events = _eventMapper.MapAll(request.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs new file mode 100644 index 000000000..c486dadb6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Events.External.Handlers +{ + public class FriendRequestCreatedHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public FriendRequestCreatedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(FriendRequestCreated @event, CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var request = new FriendRequest( + inviterId: @event.RequesterId, + inviteeId: @event.FriendId, + requestedAt: now, + state: FriendState.Requested + ); + + await _friendRepository.AddRequestAsync(request); + + var events = _eventMapper.MapAll(request.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs new file mode 100644 index 000000000..e40c59e87 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs @@ -0,0 +1,50 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; + +namespace MiniSpace.Services.Friends.Application.Events.External.Handlers +{ + public class PendingFriendAcceptedHandler : IEventHandler + { + private readonly IFriendRepository _friendRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public PendingFriendAcceptedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) + { + _friendRepository = friendRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(PendingFriendAccepted @event, CancellationToken cancellationToken) + { + // Fetch the friendship and check existence + var friendship = await _friendRepository.GetFriendshipAsync(@event.RequesterId, @event.FriendId); + if (friendship == null) + { + throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); + } + + // Confirm the friendship + friendship.MarkAsConfirmed(); + await _friendRepository.UpdateFriendshipAsync(friendship); + + // Create reciprocal friendship to ensure mutual visibility and interaction + if (await _friendRepository.GetFriendshipAsync(@event.FriendId, @event.RequesterId) == null) + { + var reciprocalFriendship = new Core.Entities.Friend(@event.FriendId, @event.RequesterId, DateTime.UtcNow, Core.Entities.FriendState.Accepted); + await _friendRepository.AddAsync(reciprocalFriendship); + reciprocalFriendship.MarkAsConfirmed(); + await _friendRepository.UpdateFriendshipAsync(reciprocalFriendship); + } + + // Publish the confirmation event + var confirmationEvent = new FriendshipConfirmed(@event.RequesterId, @event.FriendId); + await _messageBroker.PublishAsync(confirmationEvent); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs new file mode 100644 index 000000000..9e9ef8c60 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs @@ -0,0 +1,56 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using System.Text.Json; +using System; + +namespace MiniSpace.Services.Friends.Application.Events.External.Handlers +{ + public class PendingFriendDeclinedHandler : IEventHandler + { + private readonly IFriendRequestRepository _friendRequestRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + + public PendingFriendDeclinedHandler( + IFriendRequestRepository friendRequestRepository, + IEventMapper eventMapper, + IMessageBroker messageBroker, + IAppContext appContext) + { + _friendRequestRepository = friendRequestRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + _appContext = appContext; + } + + public async Task HandleAsync(PendingFriendDeclined @event, CancellationToken cancellationToken) + { + Console.WriteLine($"Handling event: {JsonSerializer.Serialize(@event)}"); + + Console.WriteLine($"Searching for friend request between {@event.RequesterId} and {@event.FriendId}"); + var friendRequest = await _friendRequestRepository.FindByInviterAndInvitee(@event.RequesterId, @event.FriendId); + + if (friendRequest == null) + { + Console.WriteLine("No friend request found, throwing exception."); + throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); + } + + if (friendRequest.State != FriendState.Declined) + { + Console.WriteLine("Friend request found but not declined, declining now."); + friendRequest.Decline(); + friendRequest.State = FriendState.Declined; + await _friendRequestRepository.UpdateAsync(friendRequest); + } + + Console.WriteLine("Publishing events related to the decline."); + // var events = _eventMapper.MapAll(friendRequest.Events); + // await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendAccepted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendAccepted.cs new file mode 100644 index 000000000..c18a19d4f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendAccepted.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class PendingFriendAccepted : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendAccepted(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendDeclined.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendDeclined.cs new file mode 100644 index 000000000..4b54c6a71 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendDeclined.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("notifications")] + public class PendingFriendDeclined : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public PendingFriendDeclined(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/UserStatusUpdated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/UserStatusUpdated.cs new file mode 100644 index 000000000..9bfbd9ebc --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/UserStatusUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Friends.Application.Events.External +{ + [Message("users")] + public class UserStatusUpdated : IEvent + { + public Guid UserId { get; } + public string Status { get; } + + public UserStatusUpdated(Guid userId, string status) + { + UserId = userId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs new file mode 100644 index 000000000..2e36a5504 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class FriendAdded : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendAdded(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRemoved.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRemoved.cs new file mode 100644 index 000000000..75a03e4e5 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRemoved.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class FriendRemoved : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendRemoved(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs new file mode 100644 index 000000000..618700354 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs @@ -0,0 +1,18 @@ +// TODO: REMOVE. + +// using Convey.CQRS.Events; + +// namespace MiniSpace.Services.Friends.Application.Events +// { +// public class FriendRequestAccepted : IEvent +// { +// public Guid RequesterId { get; } +// public Guid FriendId { get; } + +// public FriendRequestAccepted(Guid requesterId, Guid friendId) +// { +// RequesterId = requesterId; +// FriendId = friendId; +// } +// } +// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs new file mode 100644 index 000000000..2f298649a --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs @@ -0,0 +1,18 @@ +// TODO: REMOVE. + +// using Convey.CQRS.Events; + +// namespace MiniSpace.Services.Friends.Application.Events +// { +// public class FriendRequestRejected : IEvent +// { +// public Guid RequesterId { get; } +// public Guid FriendId { get; } + +// public FriendRequestRejected(Guid requesterId, Guid friendId) +// { +// RequesterId = requesterId; +// FriendId = friendId; +// } +// } +// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs new file mode 100644 index 000000000..d585d850d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events +{ + public class FriendshipConfirmed : IEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendshipConfirmed(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendAddingFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendAddingFailed.cs new file mode 100644 index 000000000..e03efdcc3 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendAddingFailed.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendAddingFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendAddingFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } + +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendInvitationFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendInvitationFailed.cs new file mode 100644 index 000000000..05815ad1e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendInvitationFailed.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendInvitationFailed : IRejectedEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendInvitationFailed(Guid inviterId, Guid inviteeId, string reason, string code) + { + InviterId = inviterId; + InviteeId = inviteeId; + Reason = reason; + Code = code; + } + } + +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRemovalFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRemovalFailed.cs new file mode 100644 index 000000000..5b0626268 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRemovalFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendRemovalFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendRemovalFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestCreationFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestCreationFailed.cs new file mode 100644 index 000000000..d67190108 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestCreationFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendRequestCreationFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendRequestCreationFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestFailed.cs new file mode 100644 index 000000000..32e613a09 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendRequestFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendRequestFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestRejectionFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestRejectionFailed.cs new file mode 100644 index 000000000..62bee4652 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestRejectionFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendRequestRejectionFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendRequestRejectionFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestSendingFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestSendingFailed.cs new file mode 100644 index 000000000..fbd277af0 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/FriendRequestSendingFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class FriendRequestSendingFailed : IRejectedEvent + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + public string Reason { get; } + public string Code { get; } + + public FriendRequestSendingFailed(Guid inviterId, Guid inviteeId, string reason, string code) + { + InviterId = inviterId; + InviteeId = inviteeId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendAcceptanceFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendAcceptanceFailed.cs new file mode 100644 index 000000000..8098732d6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendAcceptanceFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class PendingFriendAcceptanceFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public PendingFriendAcceptanceFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendDeclinationFailed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendDeclinationFailed.cs new file mode 100644 index 000000000..629837287 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/Rejected/PendingFriendDeclinationFailed.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Events.Rejected +{ + public class PendingFriendDeclinationFailed : IRejectedEvent + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string Reason { get; } + public string Code { get; } + + public PendingFriendDeclinationFailed(Guid requesterId, Guid friendId, string reason, string code) + { + RequesterId = requesterId; + FriendId = friendId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyFriendsException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyFriendsException.cs new file mode 100644 index 000000000..06f9f85d2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyFriendsException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class AlreadyFriendsException : AppException + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public override string Code { get; } = "already_friends"; + public AlreadyFriendsException(Guid requesterId, Guid friendId) + : base($"Already friends: {requesterId} and {friendId}") + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyInvitedException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyInvitedException.cs new file mode 100644 index 000000000..ca133b7c4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AlreadyInvitedException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class AlreadyInvitedException : AppException + { + public Guid InviterId { get; } + public Guid InviteeId { get; } + public override string Code { get; } = "already_invited"; + + public AlreadyInvitedException(Guid inviterId, Guid inviteeId) + : base($"A pending friend request already exists between: {inviterId} and {inviteeId}") + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AppException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..b6c071da7 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendshipNotFoundException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendshipNotFoundException.cs new file mode 100644 index 000000000..b80292f0d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/FriendshipNotFoundException.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class FriendshipNotFoundException : AppException + { + public override string Code { get; } = "friendship_not_found"; + public Guid RequesterId { get; } + public Guid FriendId { get; } + + public FriendshipNotFoundException(Guid requesterId, Guid friendId) : base($"Friendship between requester ID {requesterId} and friend ID {friendId} was not found.") + { + RequesterId = requesterId; + FriendId = friendId; + } + + public FriendshipNotFoundException(Guid friendId) : base($"Friendship involving friend ID {friendId} was not found.") + { + FriendId = friendId; + } + + } +} + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestException.cs new file mode 100644 index 000000000..6c45d7e08 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class InvalidFriendRequestException : AppException + { + public override string Code { get; } = "invalid_friend_request"; + public InvalidFriendRequestException() : base("Invalid friend request.") + { + } + } +} + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs new file mode 100644 index 000000000..7b14f309e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs new file mode 100644 index 000000000..ef79f514b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedFriendActionException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedFriendActionException.cs new file mode 100644 index 000000000..193a1cdec --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedFriendActionException.cs @@ -0,0 +1,16 @@ + +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class UnauthorizedFriendActionException : AppException + { + public Guid RequesterId { get; } + public Guid FriendId { get; } + public override string Code { get; } = "unauthorized_friend_action"; + public UnauthorizedFriendActionException(Guid requesterId, Guid friendId) : base("You are not authorized to perform this action.") + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs new file mode 100644 index 000000000..2bcbf7d72 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Application/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Extensions.cs new file mode 100644 index 000000000..4e24696d4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IAppContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IAppContext.cs new file mode 100644 index 000000000..7bff132da --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Friends.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs new file mode 100644 index 000000000..a3535849b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Friends.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.csproj b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.csproj new file mode 100644 index 000000000..6282c20bb --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.sln b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.sln new file mode 100644 index 000000000..462555bc4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/MiniSpace.Services.Friends.Application.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.Friends.Application", "MiniSpace.Services.Friends.Application.csproj", "{2EBCBB32-6132-4479-BC03-B65D1D5C45B2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2EBCBB32-6132-4479-BC03-B65D1D5C45B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EBCBB32-6132-4479-BC03-B65D1D5C45B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EBCBB32-6132-4479-BC03-B65D1D5C45B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EBCBB32-6132-4479-BC03-B65D1D5C45B2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3467B78D-1A52-42C9-A65C-3D79D504C802} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs new file mode 100644 index 000000000..4845571bf --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFriend : IQuery + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendEvents.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendEvents.cs new file mode 100644 index 000000000..e889ea492 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendEvents.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFriendEvents : IQuery + { + public Guid FriendId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs new file mode 100644 index 000000000..546f8ffbd --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Queries; +using System.Text.Json.Serialization; +using MiniSpace.Services.Friends.Application.Dto; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFriendRequests : IQuery>, IQuery + { + public Guid StudentId { get; set; } + + [JsonConstructor] + public GetFriendRequests([property: JsonPropertyName("studentId")] Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs new file mode 100644 index 000000000..3827cb48e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using System.Collections.Generic; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFriends : IQuery> + { + public Guid StudentId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs new file mode 100644 index 000000000..269671cac --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using System; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetSentFriendRequests : IQuery> + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..7f14b9286 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Friends.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..2ce1e340a --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..d7ce985c1 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Friends.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateId.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..1d65eb2ee --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..8f4f7bd2f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs new file mode 100644 index 000000000..f65bd27c2 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs @@ -0,0 +1,90 @@ +using System; +using MiniSpace.Services.Friends.Core.Events; +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class Friend : AggregateRoot + { + public Guid FriendId { get; private set; } + public Guid StudentId { get; private set; } + public FriendState FriendState { get; private set; } + public DateTime CreatedAt { get; private set; } + + public Friend(Guid studentId, Guid friendId, DateTime createdAt, FriendState state) + { + Id = Guid.NewGuid(); + StudentId = studentId; + FriendId = friendId; + CreatedAt = createdAt; + FriendState = state; + } + + public static Friend CreateNewFriendship(Guid studentId, Guid friendId) + { + return new Friend(studentId, friendId, DateTime.UtcNow, FriendState.Accepted); + } + + public void InviteFriend(Student inviter, Student invitee) + { + if (FriendState != FriendState.Unknown) + { + throw new InvalidFriendInvitationException(inviter.Id, invitee.Id); + } + FriendState = FriendState.Requested; + Friend newFriend = new Friend(inviter.Id, invitee.Id, DateTime.UtcNow, FriendState.Requested); + AddEvent(new FriendInvited(this, newFriend)); + } + + public void AcceptFriendship(Student friend) + { + if (FriendState != FriendState.Requested) + { + throw new InvalidFriendStateException(FriendId, "Friendship cannot be accepted in the current state."); + } + FriendState = FriendState.Accepted; + AddEvent(new FriendshipConfirmed(Id)); + } + + public void DeclineFriendship() + { + if (FriendState != FriendState.Requested) + throw new InvalidOperationException("Friendship can only be declined if it is in the requested state."); + + FriendState = FriendState.Declined; + AddEvent(new FriendshipDeclined(Id, FriendId)); + } + + + public void MarkAsConfirmed() + { + if (FriendState != FriendState.Requested) + throw new InvalidFriendshipStateException(Id, FriendState.ToString(), "Requested"); + + FriendState = FriendState.Confirmed; + AddEvent(new FriendshipConfirmed(FriendId)); + } + + public void MarkAsDeclined() + { + if (FriendState != FriendState.Requested) + throw new InvalidFriendshipStateException(Id, FriendState.ToString(), "Only Requested friendships can be declined."); + + FriendState = FriendState.Declined; + AddEvent(new FriendshipDeclined(Id, FriendId)); + } + + + + public void RemoveFriend(Student requester, Student friend) + { + if (FriendState != FriendState.Accepted) + { + throw new InvalidFriendStateException(FriendId, "Only accepted friendships can be removed."); + } + FriendState = FriendState.Cancelled; + + AddEvent(new FriendRemoved(requester, friend)); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs new file mode 100644 index 000000000..1a691f0e6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendRequest.cs @@ -0,0 +1,46 @@ +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class FriendRequest : AggregateRoot + { + public Guid InviterId { get; private set; } + public Guid InviteeId { get; private set; } + public DateTime RequestedAt { get; private set; } + public FriendState _state; + public FriendState State + { + get => _state; + set => _state = value; + } + + public FriendRequest(Guid inviterId, Guid inviteeId, DateTime requestedAt, FriendState state) + { + Id = Guid.NewGuid(); + InviterId = inviterId; + InviteeId = inviteeId; + RequestedAt = requestedAt; + _state = state; + } + + + public void Accept() + { + if (State != FriendState.Requested) + throw new InvalidOperationException("Only requested friend requests can be accepted."); + + _state = FriendState.Accepted; + AddEvent(new FriendshipConfirmed(Id)); + } + + public void Decline() + { + if (State != FriendState.Requested) + throw new InvalidOperationException("Only requested friend requests can be declined."); + + _state = FriendState.Declined; + AddEvent(new FriendshipDeclined(InviterId, InviteeId)); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs new file mode 100644 index 000000000..967ab25d6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/FriendState.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Friends.Core.Entities +{ + public enum FriendState + { + Unknown, + Requested, + Accepted, + Declined, + Blocked, + Cancelled, + Confirmed + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs new file mode 100644 index 000000000..f7a8d6cb7 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs @@ -0,0 +1,44 @@ +using System; +using MiniSpace.Services.Friends.Core.Events; +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class Friendship : AggregateRoot + { + public Guid RequesterId { get; private set; } + public Guid FriendId { get; private set; } + public DateTime CreatedAt { get; private set; } + public FriendState State { get; private set; } + + public Friendship(Guid requesterId, Guid friendId) + { + Id = Guid.NewGuid(); + RequesterId = requesterId; + FriendId = friendId; + State = FriendState.Requested; + CreatedAt = DateTime.UtcNow; + } + + public void MarkAsConfirmed() + { + if (State != FriendState.Requested) + throw new InvalidFriendshipStateException(Id, State.ToString(), "Requested"); + State = FriendState.Confirmed; + AddEvent(new FriendshipConfirmed(Id)); + } + + public void DeclineFriendship() + { + if (State != FriendState.Requested) + throw new InvalidOperationException("Friendship can only be declined if it is in the requested state."); + + State = FriendState.Declined; + // Assuming 'Id' is the ID of the current object and 'FriendId' is the ID of the friend. + AddEvent(new FriendshipDeclined(RequesterId, FriendId)); + } + + + } + +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs similarity index 54% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs index cfd028ec7..29af756f4 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Student.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs @@ -1,9 +1,12 @@ -namespace MiniSpace.Services.Posts.Core.Entities +namespace MiniSpace.Services.Friends.Core.Entities { public class Student { public Guid Id { get; private set; } public string FullName { get; private set; } + public string Email { get; private set; } + public string FirstName { get; private set; } + public string LastName { get; private set; } public Student(Guid id, string fullName) { @@ -11,4 +14,4 @@ public Student(Guid id, string fullName) FullName = fullName; } } -} +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs new file mode 100644 index 000000000..5ce2e7349 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendAdded : IDomainEvent + { + public Student Requester { get; private set; } + public Student Friend { get; private set; } + + public FriendAdded(Student requester, Student friend) + { + Requester = requester; + Friend = friend; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendInvited.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendInvited.cs new file mode 100644 index 000000000..a2b773130 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendInvited.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendInvited : IDomainEvent + { + public Friend Inviter { get; } + public Friend Invitee { get; } + + public FriendInvited(Friend inviter, Friend invitee) + { + Inviter = inviter; + Invitee = invitee; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs new file mode 100644 index 000000000..55e170c7d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendRemoved : IDomainEvent + { + public Student Requester { get; private set; } + public Student Friend { get; private set; } + + public FriendRemoved(Student requester, Student friend) + { + Requester = requester; + Friend = friend; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCreated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCreated.cs new file mode 100644 index 000000000..5586f7afc --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRequestCreated.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendRequestCreated : IDomainEvent + { + public FriendRequest FriendRequest { get; private set; } + + public FriendRequestCreated(FriendRequest friendRequest) + { + FriendRequest = friendRequest; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipConfirmed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipConfirmed.cs new file mode 100644 index 000000000..09f174c54 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipConfirmed.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendshipConfirmed : IDomainEvent + { + public Guid FriendId { get; } + + public FriendshipConfirmed(Guid friendId) + { + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs new file mode 100644 index 000000000..c7d11ab68 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Friends.Core.Events +{ + public class FriendshipDeclined : IDomainEvent + { + // Adding separate properties for the requester and the friend + public Guid RequesterId { get; } + public Guid FriendId { get; } + public FriendshipDeclined(Guid requesterId, Guid friendId) + { + RequesterId = requesterId; + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..810e5027c --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Friends.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..4f0c86852 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAcceptanceException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAcceptanceException.cs new file mode 100644 index 000000000..5f58b30d4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAcceptanceException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class FriendAcceptanceException : DomainException + { + public override string Code { get; } = "friend_acceptance_failure"; + public Guid RequesterId { get; } + public Guid AcceptorId { get; } + + public FriendAcceptanceException(Guid requesterId, Guid acceptorId) + : base($"Failure in accepting friend request from {requesterId} to {acceptorId}.") + { + RequesterId = requesterId; + AcceptorId = acceptorId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendDeclinationException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendDeclinationException.cs new file mode 100644 index 000000000..70627fea6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendDeclinationException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class FriendDeclinationException : DomainException + { + public override string Code { get; } = "friend_declination_failure"; + public Guid RequesterId { get; } + public Guid DeclinerId { get; } + + public FriendDeclinationException(Guid requesterId, Guid declinerId) + : base($"Declination of friend request from {requesterId} by {declinerId} failed.") + { + RequesterId = requesterId; + DeclinerId = declinerId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs new file mode 100644 index 000000000..ce636398d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs @@ -0,0 +1,20 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class FriendshipStateException : DomainException + { + public override string Code { get; } = "friendship_state_error"; + public Guid StudentId { get; } + public FriendState AttemptedState { get; } + public FriendState CurrentState { get; } + + public FriendshipStateException(Guid studentId, FriendState attemptedState, FriendState currentState) + : base($"Attempt to change friendship state to {attemptedState} from {currentState} failed for student ID {studentId}.") + { + StudentId = studentId; + AttemptedState = attemptedState; + CurrentState = currentState; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..5595680ca --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendFullNameException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendFullNameException.cs new file mode 100644 index 000000000..951615ac7 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendFullNameException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class InvalidFriendFullNameException : Exception + { + public Guid FriendId { get; } + public string FullName { get; } + + public InvalidFriendFullNameException(Guid friendId, string fullName) + : base($"Invalid full name '{fullName}' provided for friend with ID {friendId}.") + { + FriendId = friendId; + FullName = fullName; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendInvitationException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendInvitationException.cs new file mode 100644 index 000000000..39db6b127 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendInvitationException.cs @@ -0,0 +1,18 @@ +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class InvalidFriendInvitationException : DomainException + { + public override string Code { get; } = "invalid_friend_invitation"; + public Guid InviterId { get; } + public Guid InviteeId { get; } + + public InvalidFriendInvitationException(Guid inviterId, Guid inviteeId) + : base($"Invalid invitation from {inviterId} to {inviteeId}.") + { + InviterId = inviterId; + InviteeId = inviteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendRequestException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendRequestException.cs new file mode 100644 index 000000000..ddd942ddd --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendRequestException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class InvalidFriendRequestException : DomainException + { + public override string Code { get; } = "invalid_friend_request"; + public Guid RequesterId { get; } + public Guid RequesteeId { get; } + + public InvalidFriendRequestException(Guid requesterId, Guid requesteeId) + : base($"Invalid friend request from {requesterId} to {requesteeId}.") + { + RequesterId = requesterId; + RequesteeId = requesteeId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendStateException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendStateException.cs new file mode 100644 index 000000000..3f6998297 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendStateException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class InvalidFriendStateException : Exception + { + public Guid FriendId { get; } + + public InvalidFriendStateException(Guid friendId, string message) : base(message) + { + FriendId = friendId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendshipStateException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendshipStateException.cs new file mode 100644 index 000000000..b0ad2c07b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/InvalidFriendshipStateException.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class InvalidFriendshipStateException : DomainException + { + public override string Code { get; } = "invalid_friendship_state"; + public Guid FriendshipId { get; } + public string CurrentState { get; } + public string RequiredState { get; } + + public InvalidFriendshipStateException(Guid friendshipId, string currentState, string requiredState) + : base($"Cannot confirm friendship: Current state '{currentState}' does not meet required state '{requiredState}'.") + { + FriendshipId = friendshipId; + CurrentState = currentState; + RequiredState = requiredState; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.csproj b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.sln b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.sln new file mode 100644 index 000000000..cc0587691 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/MiniSpace.Services.Friends.Core.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.Friends.Core", "MiniSpace.Services.Friends.Core.csproj", "{C6185FC8-5F50-46D2-8CEA-7FC054AC69EC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C6185FC8-5F50-46D2-8CEA-7FC054AC69EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6185FC8-5F50-46D2-8CEA-7FC054AC69EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6185FC8-5F50-46D2-8CEA-7FC054AC69EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6185FC8-5F50-46D2-8CEA-7FC054AC69EC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C838E78-0FF4-4507-AAC2-22F77822D920} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs new file mode 100644 index 000000000..993323a81 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IFriendRepository + { + Task AddFriendAsync(Guid requesterId, Guid friendId); + Task> GetFriendsAsync(Guid studentId); + Task IsFriendAsync(Guid studentId, Guid potentialFriendId); + Task RemoveFriendAsync(Guid requesterId, Guid friendId); + Task AcceptFriendInvitationAsync(Guid requesterId, Guid friendId); + Task DeclineFriendInvitationAsync(Guid requesterId, Guid friendId); + Task GetFriendshipAsync(Guid requesterId, Guid friendId); + Task InviteFriendAsync(Guid inviterId, Guid inviteeId); + Task UpdateFriendshipAsync(Friend friend); + Task AddRequestAsync(FriendRequest request); + Task UpdateAsync(Friend friend); + Task AddInvitationAsync(FriendRequest invitation); + Task AddAsync(Friend friend); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRequestRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRequestRepository.cs new file mode 100644 index 000000000..2af20e49c --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRequestRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IFriendRequestRepository + { + Task GetAsync(Guid id); + Task AddAsync(FriendRequest friendRequest); + Task UpdateAsync(FriendRequest friendRequest); + Task DeleteAsync(Guid id); + Task FindByInviterAndInvitee(Guid inviterId, Guid inviteeId); + Task> GetFriendRequestsByUser(Guid userId); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs similarity index 67% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs index f5f9f020a..4e6c92d38 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IStudentRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs @@ -1,6 +1,6 @@ -using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Friends.Core.Entities; -namespace MiniSpace.Services.Posts.Core.Repositories +namespace MiniSpace.Services.Friends.Core.Repositories { public interface IStudentRepository { @@ -9,4 +9,4 @@ public interface IStudentRepository Task AddAsync(Student student); Task DeleteAsync(Guid id); } -} +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..0a40512e9 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Friends.Application; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..63c05b661 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Friends.Application; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..d9a4255aa --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..6b2c5b72c --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Friends.Application; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..d81be523c --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.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.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..31f949391 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.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.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..d125750d0 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,27 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Friends.Application.Commands; +using MiniSpace.Services.Friends.Application.Events.Rejected; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + AlreadyFriendsException ex => new FriendAddingFailed(ex.RequesterId, ex.FriendId, ex.Message, "already_friends"), + FriendshipNotFoundException ex => message switch + { + AddFriend _ => new FriendAddingFailed(ex.RequesterId, ex.FriendId, ex.Message, "friendship_not_found"), + RemoveFriend _ => new FriendRemovalFailed(ex.RequesterId, ex.FriendId, ex.Message, "friendship_not_found"), + _ => null + }, + UnauthorizedFriendActionException ex => new FriendAddingFailed(ex.RequesterId, ex.FriendId, ex.Message, "unauthorized_action"), + _ => null + }; + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..d3da6c0e8 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Core.Exceptions; + +namespace MiniSpace.Services.Friends.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.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs new file mode 100644 index 000000000..f3638bc86 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs @@ -0,0 +1,144 @@ +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.Friends.Application; +using MiniSpace.Services.Friends.Application.Commands; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Events.External.Handlers; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Contexts; +using MiniSpace.Services.Friends.Infrastructure.Decorators; +using MiniSpace.Services.Friends.Infrastructure.Exceptions; +using MiniSpace.Services.Friends.Infrastructure.Logging; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Services; +using MiniSpace.Services.Friends.Application.Events; + +namespace MiniSpace.Services.Friends.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<>)); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("friendRequests") + .AddMongoRepository("friends") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .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.Friends/src/MiniSpace.Services.Friends.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..099ae27ac --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Friends.Application; + +namespace MiniSpace.Services.Friends.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..37c5ee989 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Friends.Application.Commands; + +namespace MiniSpace.Services.Friends.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(AddFriend).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..a9d0381d6 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,57 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Friends.Application.Commands; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Events.External; + +namespace MiniSpace.Services.Friends.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(AddFriend), new HandlerLogTemplate + { + After = "Friendship added between requester: {RequesterId} and friend: {FriendId}." + } + }, + { + typeof(RemoveFriend), new HandlerLogTemplate + { + After = "Friendship removed between requester: {RequesterId} and friend: {FriendId}." + } + }, + { + typeof(InviteFriend), new HandlerLogTemplate + { + After = "Friendship invitation sent from inviter: {InviterId} to invitee: {InviteeId}." + } + }, + { + typeof(PendingFriendAccept), new HandlerLogTemplate + { + After = "Friendship acceptance processed for requester: {RequesterId} with friend: {FriendId}." + } + }, + { + typeof(PendingFriendDecline), new HandlerLogTemplate + { + After = "Friendship decline processed for requester: {RequesterId} with friend: {FriendId}." + } + }, + { + typeof(FriendRequestSent), new HandlerLogTemplate + { + After = "Friend request sent from: {InviterId} to: {InviteeId}." + } + } + }; + + 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.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.csproj b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.csproj new file mode 100644 index 000000000..c0b3d159b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.sln b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.sln new file mode 100644 index 000000000..03a9e3025 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/MiniSpace.Services.Friends.Infrastructure.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.Friends.Infrastructure", "MiniSpace.Services.Friends.Infrastructure.csproj", "{9969F9F2-D713-4756-BA28-5614326AFDED}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9969F9F2-D713-4756-BA28-5614326AFDED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9969F9F2-D713-4756-BA28-5614326AFDED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9969F9F2-D713-4756-BA28-5614326AFDED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9969F9F2-D713-4756-BA28-5614326AFDED}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {91A6AA2E-8591-445E-AE77-2E4B44E4E1DE} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..6ad98a705 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,74 @@ +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Friends.Application.Dto; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Friend AsEntity(this FriendDocument document) + => new Friend(document.StudentId, document.FriendId, document.CreatedAt, document.State); + + + + public static FriendDocument AsDocument(this Friend entity) + => new FriendDocument + { + Id = entity.Id, + StudentId = entity.StudentId, + FriendId = entity.FriendId, + CreatedAt = entity.CreatedAt, + State = entity.FriendState + }; + + public static FriendDto AsDto(this FriendDocument document) + => new FriendDto + { + Id = document.Id, + StudentId = document.StudentId, + FriendId = document.FriendId, + CreatedAt = document.CreatedAt, + State = document.State, + // Email = document.Email, + // FirstName = document.FirstName, + // LastName = document.LastName + }; + + public static FriendRequest AsEntity(this FriendRequestDocument document) + => new FriendRequest(document.InviterId, document.InviteeId, document.RequestedAt, document.State); + + public static FriendRequestDocument AsDocument(this FriendRequest entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity), "FriendRequest entity cannot be null."); + } + + if (entity.Id == null) + { + throw new InvalidOperationException("FriendRequest.Id must be initialized."); + } + Console.WriteLine($"******************************************************Friend request state {entity.State}"); + return new FriendRequestDocument + { + Id = entity.Id, + InviterId = entity.InviterId, + InviteeId = entity.InviteeId, + RequestedAt = entity.RequestedAt, + State = entity.State + }; + } + + + public static FriendRequestDto AsDto(this FriendRequestDocument document) + => new FriendRequestDto + { + Id = document.Id, + InviterId = document.InviterId, + InviteeId = document.InviteeId, + RequestedAt = document.RequestedAt, + State = document.State, + StudentId = document.InviteeId + }; + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs new file mode 100644 index 000000000..4e1d6f6c5 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs @@ -0,0 +1,18 @@ +using Convey.Types; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents +{ + public class FriendDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid FriendId { get; set; } + public Guid StudentId { get; set; } + public DateTime CreatedAt { get; set; } + public FriendState State { get; set; } + + // public string Email { get; set; } + // public string FirstName { get; set; } + // public string LastName { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendRequestDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendRequestDocument.cs new file mode 100644 index 000000000..02e58ee06 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendRequestDocument.cs @@ -0,0 +1,14 @@ +using Convey.Types; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents +{ + public class FriendRequestDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + public DateTime RequestedAt { get; set; } + public FriendState State { get; set; } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs new file mode 100644 index 000000000..4f7bd2a45 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs @@ -0,0 +1,28 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Exceptions; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFriendHandler : IQueryHandler + { + private readonly IMongoRepository _friendRepository; + + public GetFriendHandler(IMongoRepository friendRepository) + { + _friendRepository = friendRepository; + } + + public async Task HandleAsync(GetFriend query, CancellationToken cancellationToken) + { + var document = await _friendRepository.GetAsync(p => p.Id == query.StudentId); + if (document == null) + throw new FriendshipNotFoundException(query.StudentId); + + return document.AsDto(); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs new file mode 100644 index 000000000..e17f29001 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs @@ -0,0 +1,40 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using System.Text.Json; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFriendRequestsHandler : IQueryHandler> + { + private readonly IMongoRepository _friendRequestRepository; + + + public GetFriendRequestsHandler(IMongoRepository friendRequestRepository) + { + _friendRequestRepository = friendRequestRepository; + } + + public async Task> HandleAsync(GetFriendRequests query, CancellationToken cancellationToken) + { + string queryJson = JsonSerializer.Serialize(query); + Console.WriteLine($"Handling GetFriendRequests: {queryJson}"); + Console.WriteLine($"Handling GetFriendRequests for UserId: {query.StudentId}"); + + var documents = await _friendRequestRepository.FindAsync(p => p.InviteeId == query.StudentId && p.State == FriendState.Requested); + Console.WriteLine($"Found {documents.Count()} friend requests."); + + if (!documents.Any()) + { + Console.WriteLine($"No friend requests found for UserId: {query.StudentId}."); + return Enumerable.Empty(); + } + + return documents.Select(doc => doc.AsDto()); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs new file mode 100644 index 000000000..2004a2b2e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs @@ -0,0 +1,25 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFriendsHandler : IQueryHandler> + { + private readonly IMongoRepository _friendRepository; + + public GetFriendsHandler(IMongoRepository friendRepository) + { + _friendRepository = friendRepository; + } + + public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) + { + var documents = await _friendRepository.FindAsync(p => p.StudentId == query.StudentId); + + return documents.Select(doc => doc.AsDto()); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs new file mode 100644 index 000000000..2f1635aca --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs @@ -0,0 +1,52 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetSentFriendRequestsHandler : IQueryHandler> + { + private readonly IMongoRepository _friendRequestRepository; + + public GetSentFriendRequestsHandler(IMongoRepository friendRequestRepository) + { + _friendRequestRepository = friendRequestRepository; + } + + public async Task> HandleAsync(GetSentFriendRequests query, CancellationToken cancellationToken) + { + Console.WriteLine($"Fetching sent friend requests for student ID: {query.StudentId}"); + + // Define options including the cancellation token + + // Assuming FindAsync only needs the filter and uses an options object for the cancellation token + var documents = await _friendRequestRepository.FindAsync( + doc => doc.InviterId == query.StudentId + + ); + + if (!documents.Any()) { + Console.WriteLine("No documents found"); + return Enumerable.Empty(); + } + + return documents.Select(doc => new FriendRequestDto + { + Id = doc.Id, + InviterId = doc.InviterId, + InviteeId = doc.InviteeId, + RequestedAt = doc.RequestedAt, + State = doc.State + }); + } + + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs new file mode 100644 index 000000000..4658140b4 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs @@ -0,0 +1,149 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories +{ + public class FriendMongoRepository : IFriendRepository + { + private readonly IMongoRepository _repository; + private readonly IMongoRepository _friendRequestRepository; + + public FriendMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var friendDocument = await _repository.GetAsync(f => f.Id == id); + if (friendDocument == null) + throw new KeyNotFoundException("Friend not found with the specified ID."); + + return friendDocument.AsEntity(); + } + + public Task AddFriendAsync(Guid requesterId, Guid friendId) + { + var friend = new FriendDocument + { + Id = Guid.NewGuid(), + StudentId = requesterId, + FriendId = friendId, + CreatedAt = DateTime.UtcNow, + State = FriendState.Requested + }; + return _repository.AddAsync(friend); + } + + public async Task UpdateAsync(Friend friend) + { + await _repository.UpdateAsync(friend.AsDocument()); + } + + public async Task DeleteAsync(Guid id) + { + await _repository.DeleteAsync(id); + } + + public async Task ExistsAsync(Guid id) + { + var friend = await _repository.GetAsync(f => f.Id == id); + return friend != null; + } + + public async Task> GetFriendsAsync(Guid studentId) + { + var documents = await _repository.FindAsync(f => f.StudentId == studentId); + return documents?.Select(d => d.AsEntity()).ToList(); + } + + public async Task GetFriendshipAsync(Guid requesterId, Guid friendId) + { + var document = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + return document?.AsEntity(); + } + + public async Task IsFriendAsync(Guid studentId, Guid potentialFriendId) + { + var friend = await _repository.GetAsync(f => f.StudentId == studentId && f.FriendId == potentialFriendId); + return friend != null; + } + + public async Task RemoveFriendAsync(Guid requesterId, Guid friendId) + { + var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + if (friend != null) + { + await _repository.DeleteAsync(friend.Id); + } + } + + public async Task AcceptFriendInvitationAsync(Guid requesterId, Guid friendId) + { + var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + if (friend != null) + { + friend.State = FriendState.Accepted; + await _repository.UpdateAsync(friend); + } + } + + public async Task DeclineFriendInvitationAsync(Guid requesterId, Guid friendId) + { + var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + if (friend != null) + { + friend.State = FriendState.Declined; + await _repository.UpdateAsync(friend); + } + } + + public async Task InviteFriendAsync(Guid inviterId, Guid inviteeId) + { + var newFriend = new FriendDocument + { + StudentId = inviterId, + FriendId = inviteeId, + CreatedAt = DateTime.UtcNow, + State = FriendState.Requested + }; + await _repository.AddAsync(newFriend); + } + + public Task AddRequestAsync(FriendRequest request) + { + var requestDocument = request.AsDocument(); + return _friendRequestRepository.AddAsync(requestDocument); + } + + public Task AddInvitationAsync(FriendRequest invitation) + { + var invitationDocument = invitation.AsDocument(); + return _friendRequestRepository.AddAsync(invitationDocument); + } + + public async Task UpdateFriendshipAsync(Friend friend) + { + await _repository.UpdateAsync(friend.AsDocument()); + } + + public async Task AddAsync(Friend friend) + { + var document = new FriendDocument + { + Id = friend.Id, + StudentId = friend.StudentId, + FriendId = friend.FriendId, + CreatedAt = friend.CreatedAt, + State = friend.FriendState + }; + await _repository.AddAsync(document); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs new file mode 100644 index 000000000..31690bfcd --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs @@ -0,0 +1,66 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories +{ + public class FriendRequestMongoRepository : IFriendRequestRepository + { + private readonly IMongoRepository _repository; + + public FriendRequestMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var document = await _repository.GetAsync(id); + return document?.AsEntity(); + } + + public async Task AddAsync(FriendRequest friendRequest) + { + var document = friendRequest.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(FriendRequest friendRequest) + { + + var documentToUpdate = friendRequest.AsDocument(); + + documentToUpdate.State = friendRequest.State; + + Console.WriteLine("Attempting to update document in database: " + JsonSerializer.Serialize(documentToUpdate)); + await _repository.UpdateAsync(documentToUpdate); + + var documentAfterUpdate = await _repository.GetAsync(friendRequest.Id); + Console.WriteLine("Document after update: " + JsonSerializer.Serialize(documentAfterUpdate)); + } + + public async Task DeleteAsync(Guid id) + { + await _repository.DeleteAsync(id); + } + + public async Task> GetFriendRequestsByUser(Guid userId) + { + var documents = await _repository.FindAsync(fr => fr.InviterId == userId || fr.InviteeId == userId); + return documents.Select(doc => doc.AsEntity()); + } + + public async Task FindByInviterAndInvitee(Guid inviterId, Guid inviteeId) + { + var document = await _repository.FindAsync(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId); + return document.FirstOrDefault()?.AsEntity(); + } + + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..638860d8d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Friends.Application.Services; + +namespace MiniSpace.Services.Friends.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..509da8fe0 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Friends.Application.Events; +using MiniSpace.Services.Friends.Application.Events.External; +using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Events; + +namespace MiniSpace.Services.Friends.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + case Core.Events.FriendAdded e: + return new Application.Events.FriendAdded(e.Requester.Id, e.Friend.Id); + + case Core.Events.FriendRemoved e: + return new Application.Events.FriendRemoved(e.Requester.Id, e.Friend.Id); + + case Core.Events.FriendshipConfirmed e: + return new PendingFriendAccepted(e.FriendId, e.FriendId); + + case Core.Events.FriendshipDeclined e: + return new PendingFriendDeclined(e.RequesterId, e.FriendId); + + default: + return null; + } + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..0faf2333e --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.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.Friends.Application.Services; + +namespace MiniSpace.Services.Friends.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.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test1.py b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test1.py new file mode 100644 index 000000000..3c8753841 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test1.py @@ -0,0 +1,37 @@ +import requests +import json +import pytest + +class TestFriendsAPI: + base_url = "http://localhost:5004" + base_friends_url = "http://localhost:5012" + token = "" + + def authenticate_and_get_token(self): + """Authenticate and retrieve token for tests.""" + login_url = f"{self.base_url}/sign-in" + login_payload = { + "email": "friend1@email.com", + "password": "friend1" + } + response = requests.post(login_url, json=login_payload) + if response.status_code != 200: + raise Exception("Authentication failed, cannot proceed with tests.") + token_response = response.json() + self.token = token_response['accessToken'] + + def setup_method(self): + """Setup that runs before every test method.""" + self.authenticate_and_get_token() + + def test_invite_friend_already_friends_error(self): + """Test inviting a friend who is already a friend returns the correct error.""" + invite_url = f"{self.base_friends_url}/friends/b202a5fe-afbd-4894-95e3-41aff99f430c/invite" + headers = {'Authorization': f'Bearer {self.token}'} + payload = { + "inviterId": "fd0176f4-736c-49cd-b57a-523d544ae3d3", + "inviteeId": "b202a5fe-afbd-4894-95e3-41aff99f430c" + } + response = requests.post(invite_url, json=payload, headers=headers) + assert response.status_code == 400 + assert "Already friends" in response.text diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test_run.py b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test_run.py new file mode 100644 index 000000000..4864555f7 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests.P/test_run.py @@ -0,0 +1,26 @@ +import pytest +import responses +from test1 import TestFriendsAPI + +def setup_mocks(): + responses.start() + # Mock the authentication endpoint + responses.add(responses.POST, 'http://localhost:5004/sign-in', + json={'accessToken': 'mocked_token'}, status=200) + # Mock the invite endpoint with a specific scenario + responses.add(responses.POST, 'http://localhost:5012/friends/b202a5fe-afbd-4894-95e3-41aff99f430c/invite', + json={"error": "Already friends"}, status=400) + +def teardown_mocks(): + responses.stop() + responses.reset() + +def run_tests(): + setup_mocks() + result = pytest.main(['test1.py']) + teardown_mocks() + return result + +if __name__ == '__main__': + test_results = run_tests() + print(f"Test Results: {test_results}") diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/Auth/AuthResponse.cs b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/Auth/AuthResponse.cs new file mode 100644 index 000000000..ff44e539d --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/Auth/AuthResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.E2ETests.Auth +{ + public class AuthResponse + { + public string accessToken { get; set; } + public string refreshToken { get; set; } + public string role { get; set; } + public long expires { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/FriendsApiTests.cs b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/FriendsApiTests.cs new file mode 100644 index 000000000..b571c3101 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/FriendsApiTests.cs @@ -0,0 +1,129 @@ +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using MiniSpace.Services.Friends.E2ETests.Auth; + +namespace MiniSpace.Services.Friends.E2ETests +{ + public class FriendsApiTests + { + private readonly HttpClient _client; + private readonly string _baseUri = "http://localhost:5012/friends"; + + public FriendsApiTests() + { + _client = new HttpClient(); + } + + private async Task AuthenticateAndGetToken() + { + var loginPayload = new StringContent(JsonSerializer.Serialize(new + { + email = "friend1@email.com", + password = "friend1" + }), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await _client.PostAsync("http://localhost:5004/sign-in", loginPayload); + + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"Authentication failed with status code {response.StatusCode} and content {errorContent}"); + throw new HttpRequestException("Authentication failed, cannot proceed with tests."); + } + + + var responseContent = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonSerializer.Deserialize(responseContent); + + return tokenResponse.accessToken; + } + + private async Task SendFriendRequestAndGetId() + { + string accessToken = await AuthenticateAndGetToken(); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var payload = new StringContent(JsonSerializer.Serialize(new + { + inviterId = "fd0176f4-736c-49cd-b57a-523d544ae3d3", + inviteeId = "b202a5fe-afbd-4894-95e3-41aff99f430c" + }), Encoding.UTF8, "application/json"); + + HttpResponseMessage inviteResponse = await _client.PostAsync($"{_baseUri}/b202a5fe-afbd-4894-95e3-41aff99f430c/invite", payload); + + if (!inviteResponse.IsSuccessStatusCode) + { + throw new HttpRequestException("Failed to send friend request."); + } + + var responseContent = await inviteResponse.Content.ReadAsStringAsync(); + var friendRequestResponse = JsonSerializer.Deserialize(responseContent); + return friendRequestResponse.friendRequestId; + } + + [Fact] + public async Task InviteFriend_Twice_First_Succeeds_Second_Fails() + { + string accessToken = await AuthenticateAndGetToken(); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var payload = new StringContent(JsonSerializer.Serialize(new + { + inviterId = "fd0176f4-736c-49cd-b57a-523d544ae3d3", + inviteeId = "b202a5fe-afbd-4894-95e3-41aff99f430c" + }), Encoding.UTF8, "application/json"); + + // First invitation should succeed with 201 Created + HttpResponseMessage firstInviteResponse = await _client.PostAsync($"{_baseUri}/b202a5fe-afbd-4894-95e3-41aff99f430c/invite", payload); + Assert.Equal(System.Net.HttpStatusCode.Created, firstInviteResponse.StatusCode); + + // Second invitation should fail with 400 Bad Request due to already invited status + HttpResponseMessage secondInviteResponse = await _client.PostAsync($"{_baseUri}/b202a5fe-afbd-4894-95e3-41aff99f430c/invite", payload); + Assert.Equal(System.Net.HttpStatusCode.BadRequest, secondInviteResponse.StatusCode); + + var responseContent = await secondInviteResponse.Content.ReadAsStringAsync(); + Assert.Contains("already_invited", responseContent); + Assert.Contains("A pending friend request already exists between", responseContent); + } + + [Fact] + public async Task AcceptFriendRequest_Endpoint_Succeeds() + { + string accessToken = await AuthenticateAndGetToken(); + string friendRequestId = await SendFriendRequestAndGetId(); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + // Assuming that there is an endpoint to create a pending friend request which should be tested as well + // Simulate accepting a friend request + HttpResponseMessage response = await _client.GetAsync($"http://localhost:5012/friends/requests/{friendRequestId}/accept"); + + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.Contains("Request accepted", responseContent); + } + + // [Fact] + // public async Task DeclineFriendRequest_Endpoint_Succeeds() + // { + // string accessToken = await AuthenticateAndGetToken(); + // string friendRequestId = await SendFriendRequestAndGetId(); + // _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + // // Simulate declining a friend request + // HttpResponseMessage response = await _client.GetAsync($"http://localhost:5012/friends/requests/{friendRequestId}/decline"); + + // Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + + // var responseContent = await response.Content.ReadAsStringAsync(); + // Assert.Contains("Request declined", responseContent); + // } + + + } +} diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.csproj b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.csproj new file mode 100644 index 000000000..459096168 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.sln b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.sln new file mode 100644 index 000000000..4b9512613 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/MiniSpace.Services.Friends.E2ETests.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.Friends.E2ETests", "MiniSpace.Services.Friends.E2ETests.csproj", "{9366BB9E-1298-4EE1-A73D-90F2F7FA7D1B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9366BB9E-1298-4EE1-A73D-90F2F7FA7D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9366BB9E-1298-4EE1-A73D-90F2F7FA7D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9366BB9E-1298-4EE1-A73D-90F2F7FA7D1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9366BB9E-1298-4EE1-A73D-90F2F7FA7D1B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90546334-7169-4EEE-9909-061CD7DF7104} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/UnitTest1.cs b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/UnitTest1.cs new file mode 100644 index 000000000..ca15ff614 --- /dev/null +++ b/MiniSpace.Services.Friends/tests/MiniSpace.Services.Friends.E2ETests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Friends.E2ETests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/tests/tests.sln b/MiniSpace.Services.Friends/tests/tests.sln new file mode 100644 index 000000000..2fe86aa2c --- /dev/null +++ b/MiniSpace.Services.Friends/tests/tests.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.Friends.E2ETests", "MiniSpace.Services.Friends.E2ETests\MiniSpace.Services.Friends.E2ETests.csproj", "{0CB35A77-4C74-4FAA-A631-8F70C6029633}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0CB35A77-4C74-4FAA-A631-8F70C6029633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CB35A77-4C74-4FAA-A631-8F70C6029633}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CB35A77-4C74-4FAA-A631-8F70C6029633}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CB35A77-4C74-4FAA-A631-8F70C6029633}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {84D57407-C66C-4A67-AD2F-62B3AE5950F1} + EndGlobalSection +EndGlobal 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 c56bd3156..b67f96d5f 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 @@ -68,14 +68,15 @@ public async Task SignInAsync(SignIn command) throw new InvalidCredentialsException(command.Email); } - var claims = user.Permissions.Any() - ? new Dictionary> - { - ["permissions"] = user.Permissions, - ["name"] = new [] { user.Name }, - ["e-mail"] = new [] { user.Email } - } - : null; + var claims = new Dictionary> + { + ["name"] = new[] { user.Name }, + ["e-mail"] = new[] { user.Email } + }; + if(user.Permissions.Any()) + { + claims.Add("permissions", user.Permissions); + } var auth = _jwtProvider.Create(user.Id, user.Role, claims: claims); auth.RefreshToken = await _refreshTokenService.CreateAsync(user.Id); diff --git a/MiniSpace.Services.Organizations/Dockerfile b/MiniSpace.Services.Organizations/Dockerfile new file mode 100644 index 000000000..1c8115fb3 --- /dev/null +++ b/MiniSpace.Services.Organizations/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Organizations.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.Organizations.Api.dll"] diff --git a/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln b/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.sln new file mode 100644 index 000000000..ff08f2bbe --- /dev/null +++ b/MiniSpace.Services.Organizations/MiniSpace.Services.Organizations.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", "{9A2329ED-4000-46C9-B9CD-956F6DA767CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Api", "src\MiniSpace.Services.Organizations.Api\MiniSpace.Services.Organizations.Api.csproj", "{D8843565-EA51-407D-9F3B-BC0741B104A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Application", "src\MiniSpace.Services.Organizations.Application\MiniSpace.Services.Organizations.Application.csproj", "{299EC532-840F-4ED9-A98F-CDC7C4DD315E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Core", "src\MiniSpace.Services.Organizations.Core\MiniSpace.Services.Organizations.Core.csproj", "{550E45D1-9BE7-4AE1-BA82-DDF57F436BEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Organizations.Infrastructure", "src\MiniSpace.Services.Organizations.Infrastructure\MiniSpace.Services.Organizations.Infrastructure.csproj", "{3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}" +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 + {D8843565-EA51-407D-9F3B-BC0741B104A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8843565-EA51-407D-9F3B-BC0741B104A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8843565-EA51-407D-9F3B-BC0741B104A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8843565-EA51-407D-9F3B-BC0741B104A0}.Release|Any CPU.Build.0 = Release|Any CPU + {299EC532-840F-4ED9-A98F-CDC7C4DD315E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {299EC532-840F-4ED9-A98F-CDC7C4DD315E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {299EC532-840F-4ED9-A98F-CDC7C4DD315E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {299EC532-840F-4ED9-A98F-CDC7C4DD315E}.Release|Any CPU.Build.0 = Release|Any CPU + {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB}.Release|Any CPU.Build.0 = Release|Any CPU + {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D8843565-EA51-407D-9F3B-BC0741B104A0} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} + {299EC532-840F-4ED9-A98F-CDC7C4DD315E} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} + {550E45D1-9BE7-4AE1-BA82-DDF57F436BEB} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} + {3C5F46E5-9CF0-4A0A-AF56-9F1D455E7D1D} = {9A2329ED-4000-46C9-B9CD-956F6DA767CB} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Organizations/scripts/build.sh b/MiniSpace.Services.Organizations/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Organizations/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Organizations/scripts/dockerize-tag-push.sh new file mode 100644 index 000000000..f5a4d7c90 --- /dev/null +++ b/MiniSpace.Services.Organizations/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.organizations:latest . + +docker tag minispace.services.organizations:latest adrianvsaint/minispace.services.organizations:latest + +docker push adrianvsaint/minispace.services.organizations:latest diff --git a/MiniSpace.Services.Organizations/scripts/start.sh b/MiniSpace.Services.Organizations/scripts/start.sh new file mode 100644 index 000000000..57c0ba53c --- /dev/null +++ b/MiniSpace.Services.Organizations/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Organizations.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/scripts/test.sh b/MiniSpace.Services.Organizations/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Organizations/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/MiniSpace.Services.Organizations.Api.csproj b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/MiniSpace.Services.Organizations.Api.csproj new file mode 100644 index 000000000..cfce016c7 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/MiniSpace.Services.Organizations.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + disable + enable + true + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs new file mode 100644 index 000000000..a2a623dda --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs @@ -0,0 +1,49 @@ +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.Organizations.Application; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure; + +namespace MiniSpace.Services.Organizations.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("organizations/{organizationId}") + .Get("organizations/{organizationId}/details") + .Get>("organizations/organizer/{organizerId}") + .Get>("organizations/root") + .Get>("organizations/{organizationId}/children") + .Post("organizations", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/root")) + .Post("organizations/{organizationId}/organizer") + .Delete("organizations/{organizationId}/organizer/{organizerId}") + )) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Properties/launchSettings.json b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Properties/launchSettings.json new file mode 100644 index 000000000..fddb8b49a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5015" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Organizations": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5015", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.Development.json b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.Development.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.docker.json b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.docker.json new file mode 100644 index 000000000..f047f298a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Organizations Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "organizations-service", + "address": "organizations-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "organizations-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": "organizations", + "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": "organizations-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "organizations-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": "organizations" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "organizations-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "organizations:" + }, + "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.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.json b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.local.json b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.local.json new file mode 100644 index 000000000..2528dbe3a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Organizations Service", + "service": "organizations-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "organizations-service", + "address": "docker.for.win.localhost", + "port": "5015", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "organizations-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": "organizations", + "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": "organizations-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "organizations-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": "organizations" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "organizations-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "organizations:" + }, + "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": "organizations-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "organizations-service", + "commonName": "organizations-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "organizations-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs new file mode 100644 index 000000000..dca309f06 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganization.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class AddOrganization: ICommand + { + public Guid OrganizationId { get; } + public string Name { get; } + public Guid ParentId { get; } + + public AddOrganization(Guid organizationId, string name, Guid parentId) + { + OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; + Name = name; + ParentId = parentId; + } + } +} + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs new file mode 100644 index 000000000..d50e78262 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class AddOrganizerToOrganization: ICommand + { + public Guid OrganizationId { get; set; } + public Guid OrganizerId { get; set; } + + public AddOrganizerToOrganization(Guid organizationId, Guid organizerId) + { + OrganizationId = organizationId; + OrganizerId = organizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs new file mode 100644 index 000000000..ede5a3d92 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizationHandler.cs @@ -0,0 +1,41 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class AddOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IAppContext _appContext; + + public AddOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext) + { + _organizationRepository = organizationRepository; + _appContext = appContext; + } + + public async Task HandleAsync(AddOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && !identity.IsAdmin) + { + throw new Exceptions.UnauthorizedAccessException("admin"); + } + + var organization = new Organization(command.OrganizationId, command.Name, command.ParentId); + if(command.ParentId != Guid.Empty) + { + var parent = await _organizationRepository.GetAsync(command.ParentId); + if(parent is null) + { + throw new ParentOrganizationNotFoundException(command.ParentId); + } + parent.MakeParent(); + await _organizationRepository.UpdateAsync(parent); + } + await _organizationRepository.AddAsync(organization); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs new file mode 100644 index 000000000..d38e6b2a4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs @@ -0,0 +1,51 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Events; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Repositories; +using UnauthorizedAccessException = System.UnauthorizedAccessException; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class AddOrganizerToOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizerRepository _organizerRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public AddOrganizerToOrganizationHandler(IOrganizationRepository organizationRepository, + IOrganizerRepository organizerRepository, IAppContext appContext, IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizerRepository = organizerRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AddOrganizerToOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (identity.IsAuthenticated && !identity.IsAdmin) + { + throw new Exceptions.UnauthorizedAccessException("admin"); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization is null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var organizer = await _organizerRepository.GetAsync(command.OrganizerId); + if (organizer is null) + { + throw new OrganizerNotFoundException(command.OrganizerId); + } + + organization.AddOrganizer(command.OrganizerId); + await _organizationRepository.UpdateAsync(organization); + await _messageBroker.PublishAsync(new OrganizerAddedToOrganization(organization.Id, organizer.Id)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs new file mode 100644 index 000000000..341fca7c5 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs @@ -0,0 +1,48 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class RemoveOrganizerFromOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizerRepository _organizerRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public RemoveOrganizerFromOrganizationHandler(IOrganizationRepository organizationRepository, + IOrganizerRepository organizerRepository, IAppContext appContext, IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizerRepository = organizerRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(RemoveOrganizerFromOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && !identity.IsAdmin) + { + throw new Exceptions.UnauthorizedAccessException("admin"); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if(organization is null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var organizer = await _organizerRepository.GetAsync(command.OrganizerId); + if(organizer is null) + { + throw new OrganizerNotFoundException(command.OrganizerId); + } + + organization.RemoveOrganizer(organizer.Id); + await _organizationRepository.UpdateAsync(organization); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs new file mode 100644 index 000000000..8f553940e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class RemoveOrganizerFromOrganization : ICommand + { + public Guid OrganizationId { get; set; } + public Guid OrganizerId { get; set; } + + public RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId) + { + OrganizationId = organizationId; + OrganizerId = organizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/ContractAttribute.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/ContractAttribute.cs new file mode 100644 index 000000000..7136f171e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Organizations.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs new file mode 100644 index 000000000..a25f6c34e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs @@ -0,0 +1,13 @@ +using System.Collections; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationDetailsDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + public IEnumerable Organizers { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs new file mode 100644 index 000000000..50ee020cc --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + } +} + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs new file mode 100644 index 000000000..c267c0b4b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Events.External.Handlers +{ + public class OrganizerRightsGrantedHandler : IEventHandler + { + private readonly IOrganizerRepository _organizerRepository; + + public OrganizerRightsGrantedHandler(IOrganizerRepository organizerRepository) + { + _organizerRepository = organizerRepository; + } + + public async Task HandleAsync(OrganizerRightsGranted @event, CancellationToken cancellationToken) + { + if (await _organizerRepository.ExistsAsync(@event.UserId)) + { + throw new OrganizerAlreadyAddedException(@event.UserId); + } + + await _organizerRepository.AddAsync(new Organizer(@event.UserId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs new file mode 100644 index 000000000..bf50906d4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Core.Repositories; + +namespace MiniSpace.Services.Organizations.Application.Events.External.Handlers +{ + public class OrganizerRightsRevokedHandler : IEventHandler + { + private readonly IOrganizerRepository _organizerRepository; + private readonly IOrganizationRepository _organizationRepository; + + public OrganizerRightsRevokedHandler(IOrganizerRepository organizerRepository, IOrganizationRepository organizationRepository) + { + _organizerRepository = organizerRepository; + _organizationRepository = organizationRepository; + } + + public async Task HandleAsync(OrganizerRightsRevoked @event, CancellationToken cancellationToken) + { + var organizer = await _organizerRepository.GetAsync(@event.UserId); + if (organizer is null) + { + throw new OrganizerNotFoundException(@event.UserId); + } + + var organizerOrganizations = await _organizationRepository.GetOrganizerOrganizationsAsync(@event.UserId); + foreach (var organization in organizerOrganizations) + { + organization.RemoveOrganizer(organizer.Id); + await _organizationRepository.UpdateAsync(organization); + } + + await _organizerRepository.DeleteAsync(@event.UserId); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsGranted.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsGranted.cs new file mode 100644 index 000000000..771c3bb87 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsGranted.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.Events.External +{ + [Message("identity") ] + 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.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsRevoked.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsRevoked.cs new file mode 100644 index 000000000..90c7a2f4d --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsRevoked.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Organizations.Application.Events.External +{ + [Message("identity")] + 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.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizerAddedToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizerAddedToOrganization.cs new file mode 100644 index 000000000..82e1470a6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizerAddedToOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class OrganizerAddedToOrganization: IEvent + { + public Guid OrganizationId { get; } + public Guid OrganizerId { get; } + + public OrganizerAddedToOrganization(Guid organizationId, Guid organizerId) + { + OrganizationId = organizationId; + OrganizerId = organizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/AppException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..44cbe5715 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizationNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizationNotFoundException.cs new file mode 100644 index 000000000..c0ee06bc7 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizationNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class OrganizationNotFoundException : AppException + { + public override string Code { get; } = "organization_not_found"; + public Guid OrganizationId { get; } + + public OrganizationNotFoundException(Guid organizationId) : base($"Organization with ID: '{organizationId}' was not found.") + { + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerAlreadyAddedException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerAlreadyAddedException.cs new file mode 100644 index 000000000..37653a2fa --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerAlreadyAddedException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class OrganizerAlreadyAddedException : AppException + { + public override string Code { get; } = "organizer_already_added"; + public Guid OrganizerId { get; } + + public OrganizerAlreadyAddedException(Guid organizerId) : base($"Organizer with ID: '{organizerId}' was already added.") + { + OrganizerId = organizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerNotFoundException.cs new file mode 100644 index 000000000..9abb900be --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/OrganizerNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class OrganizerNotFoundException : AppException + { + public override string Code { get; } = "organizer_not_found"; + public Guid OrganizerId { get; } + + public OrganizerNotFoundException(Guid organizerId) : base($"Organizer with ID: {organizerId} was not found.") + { + OrganizerId = organizerId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/ParentOrganizationNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/ParentOrganizationNotFoundException.cs new file mode 100644 index 000000000..3c369f381 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/ParentOrganizationNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class ParentOrganizationNotFoundException : AppException + { + public override string Code { get; } = "parent_organization_not_found"; + public Guid ParentId { get; } + + public ParentOrganizationNotFoundException(Guid parentId) : base($"Parent organization with ID: '{parentId}' was not found.") + { + ParentId = parentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UnauthorizedAccessException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UnauthorizedAccessException.cs new file mode 100644 index 000000000..e756e940a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UnauthorizedAccessException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class UnauthorizedAccessException : AppException + { + public override string Code { get; } = "unauthorized_access"; + public string Role { get; } + + public UnauthorizedAccessException(string role) : base($"Unauthorized access. Required role: `{role}`") + { + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Extensions.cs new file mode 100644 index 000000000..3401373aa --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IAppContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IAppContext.cs new file mode 100644 index 000000000..b74d6a8d8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Organizations.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IIdentityContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IIdentityContext.cs new file mode 100644 index 000000000..179350ac1 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Organizations.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/MiniSpace.Services.Organizations.Application.csproj b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/MiniSpace.Services.Organizations.Application.csproj new file mode 100644 index 000000000..17363c346 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/MiniSpace.Services.Organizations.Application.csproj @@ -0,0 +1,21 @@ + + + + + + + + net8.0 + enable + disable + + + + + + + + + + + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs new file mode 100644 index 000000000..a71d10e0a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetChildrenOrganizations.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetChildrenOrganizations: IQuery> + { + public Guid ParentId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs new file mode 100644 index 000000000..cbcb88480 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetOrganization : IQuery + { + public Guid OrganizationId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs new file mode 100644 index 000000000..46b266a07 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationDetails.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetOrganizationDetails : IQuery + { + public Guid OrganizationId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs new file mode 100644 index 000000000..327e8da53 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizerOrganizations.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetOrganizerOrganizations: IQuery> + { + public Guid OrganizerId { get; set; } + } +} + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs new file mode 100644 index 000000000..ca798f969 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetRootOrganizations.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetRootOrganizations: IQuery> + { + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..2bcc5a058 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Organizations.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IEventMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..8f67b1913 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Core.Events; + +namespace MiniSpace.Services.Organizations.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..0270ca9fd --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateId.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..a12be1ec3 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Organizations.Core.Exceptions; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..5bbc51055 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Organizations.Core.Events; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs new file mode 100644 index 000000000..ff915096d --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs @@ -0,0 +1,51 @@ +using MiniSpace.Services.Organizations.Core.Exceptions; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class Organization : AggregateRoot + { + private ISet _organizers = new HashSet(); + public string Name { get; private set; } + public Guid ParentId { get; private set; } + public bool IsLeaf { get; private set; } + + public IEnumerable Organizers + { + get => _organizers; + private set => _organizers = new HashSet(value); + } + + public Organization(Guid id, string name, Guid parentId, bool isLeaf = true, IEnumerable organizers = null) + { + Id = id; + Name = name; + ParentId = parentId; + IsLeaf = isLeaf; + Organizers = organizers ?? Enumerable.Empty(); + } + + public void RemoveOrganizer(Guid organizerId) + { + var organizer = _organizers.SingleOrDefault(x => x.Id == organizerId); + if(organizer is null) + { + throw new OrganizerIsNotInOrganization(organizerId, Id); + } + _organizers.Remove(organizer); + } + + public void AddOrganizer(Guid organizerId) + { + if(Organizers.Any(x => x.Id == organizerId)) + { + throw new OrganizerAlreadyAddedToOrganizationException(organizerId, Id); + } + _organizers.Add(new Organizer(organizerId)); + } + + + + public void MakeParent() + => IsLeaf = false; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organizer.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organizer.cs new file mode 100644 index 000000000..949e41f08 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organizer.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class Organizer + { + public Guid Id { get; private set; } + + public Organizer(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..63df01d9b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Organizations.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..cc1779c86 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..02da4b2b6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerAlreadyAddedToOrganizationException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerAlreadyAddedToOrganizationException.cs new file mode 100644 index 000000000..8406b71fa --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerAlreadyAddedToOrganizationException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class OrganizerAlreadyAddedToOrganizationException : DomainException + { + public override string Code { get; } = "organizer_already_added_to_organization"; + public Guid OrganizerId { get; } + public Guid OrganizationId { get; } + + public OrganizerAlreadyAddedToOrganizationException(Guid organizerId, Guid organizationId) + : base($"Organizer with ID: '{organizerId}' was already added to organization with ID: '{organizationId}'.") + { + OrganizerId = organizerId; + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerIsNotInOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerIsNotInOrganization.cs new file mode 100644 index 000000000..843b6e75e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerIsNotInOrganization.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class OrganizerIsNotInOrganization : DomainException + { + public override string Code { get; } = "organizer_is_not_in_organization"; + public Guid OrganizerId { get; } + public Guid OrganizationId { get; } + + public OrganizerIsNotInOrganization(Guid organizerId, Guid organizationId) + : base($"Organizer with ID: '{organizerId}' is not in organization with ID: '{organizationId}'.") + { + OrganizerId = organizerId; + OrganizationId = organizationId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/MiniSpace.Services.Organizations.Core.csproj b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/MiniSpace.Services.Organizations.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/MiniSpace.Services.Organizations.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRepository.cs new file mode 100644 index 000000000..2e13079e1 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRepository.cs @@ -0,0 +1,13 @@ +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationRepository + { + Task GetAsync(Guid id); + Task> GetOrganizerOrganizationsAsync(Guid organizerId); + Task AddAsync(Organization organization); + Task UpdateAsync(Organization organization); + Task DeleteAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizerRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizerRepository.cs new file mode 100644 index 000000000..5140e97a8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizerRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizerRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Organizer student); + Task DeleteAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..ccb1647b6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Organizations.Application; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..ac2758b97 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Organizations.Application; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..cc5f8b300 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..cc69bd583 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Organizations.Application; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..4fa3dff8b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.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.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..478dcf8ea --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.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.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..c69dff3a0 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,19 @@ +using Convey.MessageBrokers.RabbitMQ; +// using MiniSpace.Services.Organizations.Application.Commands; +// using MiniSpace.Services.Organizations.Application.Events.Rejected; +// using MiniSpace.Services.Organizations.Application.Events.External; +// using MiniSpace.Services.Organizations.Application.Exceptions; +// using MiniSpace.Services.Organizations.Core; + +namespace MiniSpace.Services.Organizations.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + _ => null + }; + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..a0d24d1b1 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Core.Exceptions; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs new file mode 100644 index 000000000..f020d0ce7 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs @@ -0,0 +1,136 @@ +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.Organizations.Application; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Events.External; +using MiniSpace.Services.Organizations.Application.Events.External.Handlers; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Contexts; +using MiniSpace.Services.Organizations.Infrastructure.Decorators; +using MiniSpace.Services.Organizations.Infrastructure.Exceptions; +using MiniSpace.Services.Organizations.Infrastructure.Logging; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Services; + +namespace MiniSpace.Services.Organizations.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<>)); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("organizations") + .AddMongoRepository("organizers") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..be2c5c08b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Organizations.Application; + +namespace MiniSpace.Services.Organizations.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..827297347 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Organizations.Application.Commands; + +namespace MiniSpace.Services.Organizations.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(AddOrganization).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..598978f10 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,50 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Organizations.Application.Commands; +using MiniSpace.Services.Organizations.Application.Events.External; + +namespace MiniSpace.Services.Organizations.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(AddOrganization), new HandlerLogTemplate + { + After = "Added a new organization with id: {OrganizationId}." + } + }, + { + typeof(AddOrganizerToOrganization), new HandlerLogTemplate + { + After = "Added an organizer with id: {OrganizerId} to the organization with id: {OrganizationId}." + } + }, + { + typeof(RemoveOrganizerFromOrganization), new HandlerLogTemplate + { + After = "Removed an organizer with id: {OrganizerId} from the organization with id: {OrganizationId}." + } + }, + { + typeof(OrganizerRightsGranted), new HandlerLogTemplate + { + After = "Created an organizer with id: {UserId}." + } + }, + { + typeof(OrganizerRightsRevoked), new HandlerLogTemplate + { + After = "Deleted an organizer 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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/MiniSpace.Services.Organizations.Infrastructure.csproj b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/MiniSpace.Services.Organizations.Infrastructure.csproj new file mode 100644 index 000000000..41419e932 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/MiniSpace.Services.Organizations.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + + + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..6322ec5c9 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,49 @@ +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Organization AsEntity(this OrganizationDocument document) + => new Organization(document.Id, document.Name, document.ParentId, document.IsLeaf, document.Organizers); + + public static OrganizationDocument AsDocument(this Organization entity) + => new OrganizationDocument() + { + Id = entity.Id, + Name = entity.Name, + ParentId = entity.ParentId, + IsLeaf = entity.IsLeaf, + Organizers = entity.Organizers + }; + + public static OrganizationDto AsDto(this OrganizationDocument document) + => new OrganizationDto() + { + Id = document.Id, + Name = document.Name, + ParentId = document.ParentId, + IsLeaf = document.IsLeaf + }; + + public static OrganizationDetailsDto AsDetailsDto(this OrganizationDocument document) + => new OrganizationDetailsDto() + { + Id = document.Id, + Name = document.Name, + ParentId = document.ParentId, + IsLeaf = document.IsLeaf, + Organizers = document.Organizers.Select(x => x.Id) + }; + + public static Organizer AsEntity(this OrganizerDocument document) + => new Organizer(document.Id); + + public static OrganizerDocument AsDocument(this Organizer entity) + => new OrganizerDocument() + { + Id = entity.Id + }; + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs new file mode 100644 index 000000000..24946e59b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationDocument.cs @@ -0,0 +1,14 @@ +using Convey.Types; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + public class OrganizationDocument: IIdentifiable + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + public IEnumerable Organizers { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs new file mode 100644 index 000000000..8e6411cee --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + public class OrganizerDocument : IIdentifiable + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs new file mode 100644 index 000000000..ab868a7de --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetChildrenOrganizationsHandler.cs @@ -0,0 +1,24 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetChildrenOrganizationsHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + + public GetChildrenOrganizationsHandler(IMongoRepository repository) + => _repository = repository; + + public async Task> HandleAsync(GetChildrenOrganizations query, CancellationToken cancellationToken) + { + var organizations = await _repository.FindAsync(o => o.ParentId == query.ParentId); + + return organizations.Select(o => o.AsDto()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs new file mode 100644 index 000000000..dd249addd --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationDetailsHandler.cs @@ -0,0 +1,26 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOrganizationDetailsHandler : IQueryHandler + { + private readonly IMongoRepository _repository; + + public GetOrganizationDetailsHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(GetOrganizationDetails query, CancellationToken cancellationToken) + { + var organization = await _repository.GetAsync(query.OrganizationId); + + return organization?.AsDetailsDto(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs new file mode 100644 index 000000000..d8cb184f9 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationHandler.cs @@ -0,0 +1,25 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOrganizationHandler : IQueryHandler + { + private readonly IMongoRepository _repository; + + public GetOrganizationHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(GetOrganization query, CancellationToken cancellationToken) + { + var organization = await _repository.GetAsync(query.OrganizationId); + + return organization?.AsDto(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs new file mode 100644 index 000000000..0a587fbf6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOrganizerOrganizationsHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + private readonly IAppContext _appContext; + + public GetOrganizerOrganizationsHandler(IMongoRepository repository, IAppContext appContext) + { + _repository = repository; + _appContext = appContext; + } + + public async Task> HandleAsync(GetOrganizerOrganizations query, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if ((identity.Id != query.OrganizerId || !identity.IsOrganizer) && !identity.IsAdmin) + { + return Enumerable.Empty(); + } + + var organizations = await _repository + .FindAsync(o => o.Organizers.Any(x => x.Id == query.OrganizerId)); + + return organizations.Select(o => o.AsDto()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs new file mode 100644 index 000000000..a4ca6c3c4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetRootOrganizationsHandler.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetRootOrganizationsHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + + public GetRootOrganizationsHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task> HandleAsync(GetRootOrganizations query, CancellationToken cancellationToken) + => (await _repository.FindAsync(o => o.ParentId == Guid.Empty)).Select(o => o.AsDto()); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs new file mode 100644 index 000000000..452841d0e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMongoRepository.cs @@ -0,0 +1,41 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + public class OrganizationMongoRepository : IOrganizationRepository + { + private readonly IMongoRepository _repository; + + public OrganizationMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var organization = await _repository.GetAsync(o => o.Id == id); + + return organization?.AsEntity(); + } + + public async Task> GetOrganizerOrganizationsAsync(Guid organizerId) + { + var organizations = await _repository.FindAsync(o + => o.Organizers.Any(x => x.Id == organizerId)); + + return organizations?.Select(o => o.AsEntity()); + } + + public Task AddAsync(Organization organization) + => _repository.AddAsync(organization.AsDocument()); + + public Task UpdateAsync(Organization organization) + => _repository.UpdateAsync(organization.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs new file mode 100644 index 000000000..bc7cc7973 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + public class OrganizerMongoRepository : IOrganizerRepository + { + private readonly IMongoRepository _repository; + + public OrganizerMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + public async Task GetAsync(Guid id) + { + var organizer = await _repository.GetAsync(o => o.Id == id); + + return organizer?.AsEntity(); + } + public Task ExistsAsync(Guid id) => _repository.ExistsAsync(o => o.Id == id); + public Task AddAsync(Organizer organizer) => _repository.AddAsync(organizer.AsDocument()); + public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..0da02bcd1 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Organizations.Application.Services; + +namespace MiniSpace.Services.Organizations.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..7b2ca7171 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Application.Services; +using MiniSpace.Services.Organizations.Core; +using MiniSpace.Services.Organizations.Core.Events; + +namespace MiniSpace.Services.Organizations.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.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..2390268f8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.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.Organizations.Application.Services; + +namespace MiniSpace.Services.Organizations.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/Dockerfile b/MiniSpace.Services.Posts/Dockerfile new file mode 100644 index 000000000..aaa0eda36 --- /dev/null +++ b/MiniSpace.Services.Posts/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Posts.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.Posts.Api.dll"] diff --git a/MiniSpace.Services.Posts/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Posts/scripts/dockerize-tag-push.sh new file mode 100644 index 000000000..a6282133e --- /dev/null +++ b/MiniSpace.Services.Posts/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.posts:latest . + +docker tag minispace.services.posts:latest adrianvsaint/minispace.services.posts:latest + +docker push adrianvsaint/minispace.services.posts:latest diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs index aa979769d..bdbec8ae4 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs @@ -31,7 +31,9 @@ public static async Task Main(string[] args) .UseInfrastructure() .UseDispatcherEndpoints(endpoints => endpoints .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get("posts/{postId}") .Get>("posts") + .Get>("posts/organizer/{organizerId}") .Put("posts/{postId}") .Delete("posts/{postId}") .Post("posts", diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.docker.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.docker.json new file mode 100644 index 000000000..07c6d2bc2 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Posts Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "posts-service", + "address": "posts-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "posts-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": "posts", + "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": "posts-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "posts-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": "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": "redis", + "instance": "posts:" + }, + "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.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json index b7da5b832..29f59a9ca 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/appsettings.local.json @@ -103,7 +103,7 @@ "seed": false }, "outbox": { - "enabled": true, + "enabled": false, "type": "sequential", "expiry": 3600, "intervalMilliseconds": 2000, 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 index 8312bdd00..0291c545f 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs @@ -6,18 +6,18 @@ public class CreatePost : ICommand { public Guid PostId { get; } public Guid EventId { get; } - public Guid StudentId { get; } + public Guid OrganizerId { 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, + public CreatePost(Guid postId, Guid eventId, Guid organizerId, string textContent, string mediaContent, string state, DateTime? publishDate) { PostId = postId == Guid.Empty ? Guid.NewGuid() : postId; EventId = eventId; - StudentId = studentId; + OrganizerId = organizerId; TextContent = textContent; MediaContent = mediaContent; State = state; 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 index c18744f10..63d896bac 100644 --- 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 @@ -33,7 +33,7 @@ public async Task HandleAsync(ChangePostState command, CancellationToken cancell } var identity = _appContext.Identity; - if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + if (identity.IsAuthenticated && identity.Id != post.OrganizerId && !identity.IsAdmin) { throw new UnauthorizedPostAccessException(command.PostId, identity.Id); } @@ -54,25 +54,22 @@ public async Task HandleAsync(ChangePostState command, CancellationToken cancell } var previousState = post.State.ToString().ToLowerInvariant(); + var now = _dateTimeProvider.Now; switch (newState) { case State.ToBePublished: post.SetToBePublished(command.PublishDate - ?? throw new PublishDateNullException(command.PostId, newState), - _dateTimeProvider.Now); + ?? throw new PublishDateNullException(command.PostId, newState), now); break; case State.Published: - post.SetPublished(); + post.SetPublished(now); break; case State.InDraft: - post.SetInDraft(); - break; - case State.Hidden: - post.SetHidden(); + post.SetInDraft(now); break; case State.Reported: - post.SetReported(); + post.SetReported(now); break; default: throw new InvalidPostStateException(post.State.ToString().ToLowerInvariant()); 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 index 8e42c2db3..132d00a84 100644 --- 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 @@ -11,24 +11,33 @@ namespace MiniSpace.Services.Posts.Application.Commands.Handlers public class CreatePostHandler : ICommandHandler { private readonly IPostRepository _postRepository; - private readonly IStudentRepository _studentRepository; + private readonly IEventRepository _eventRepository; private readonly IDateTimeProvider _dateTimeProvider; private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; - public CreatePostHandler(IPostRepository postRepository, IStudentRepository studentRepository, - IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker) + public CreatePostHandler(IPostRepository postRepository, IEventRepository eventRepository, + IDateTimeProvider dateTimeProvider, IMessageBroker messageBroker, IAppContext appContext) { _postRepository = postRepository; - _studentRepository = studentRepository; + _eventRepository = eventRepository; _dateTimeProvider = dateTimeProvider; _messageBroker = messageBroker; + _appContext = appContext; } public async Task HandleAsync(CreatePost command, CancellationToken cancellationToken = default) { - if (!(await _studentRepository.ExistsAsync(command.StudentId))) + var @event = await _eventRepository.GetAsync(command.EventId); + if (@event is null) { - throw new StudentNotFoundException(command.StudentId); + throw new EventNotFoundException(command.EventId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && (identity.Id != command.OrganizerId || identity.Id != @event.OrganizerId)) + { + throw new UnauthorizedPostCreationAttemptException(identity.Id, command.EventId); } if (!Enum.TryParse(command.State, true, out var newState)) @@ -38,13 +47,13 @@ public async Task HandleAsync(CreatePost command, CancellationToken cancellation switch (newState) { - case State.Hidden or State.Reported: + case 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, + var post = Post.Create(command.PostId, command.EventId, command.OrganizerId, command.TextContent, command.MediaContent, _dateTimeProvider.Now, newState, command.PublishDate); await _postRepository.AddAsync(post); 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 index 5bfae6f12..1430155a5 100644 --- 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 @@ -30,7 +30,7 @@ public async Task HandleAsync(DeletePost command, CancellationToken cancellation } var identity = _appContext.Identity; - if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + if (identity.IsAuthenticated && identity.Id != post.OrganizerId && !identity.IsAdmin) { throw new UnauthorizedPostAccessException(command.PostId, identity.Id); } 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 index 7cd591fea..f14af1fab 100644 --- 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 @@ -12,13 +12,15 @@ public class UpdatePostHandler : ICommandHandler private readonly IPostRepository _postRepository; private readonly IAppContext _appContext; private readonly IMessageBroker _messageBroker; + private readonly IDateTimeProvider _dateTimeProvider; public UpdatePostHandler(IPostRepository postRepository, IAppContext appContext, - IMessageBroker messageBroker) + IMessageBroker messageBroker, IDateTimeProvider dateTimeProvider) { _postRepository = postRepository; _appContext = appContext; _messageBroker = messageBroker; + _dateTimeProvider = dateTimeProvider; } public async Task HandleAsync(UpdatePost command, CancellationToken cancellationToken = default) @@ -30,7 +32,7 @@ public async Task HandleAsync(UpdatePost command, CancellationToken cancellation } var identity = _appContext.Identity; - if (identity.IsAuthenticated && identity.Id != post.StudentId && !identity.IsAdmin) + if (identity.IsAuthenticated && identity.Id != post.OrganizerId && !identity.IsAdmin) { throw new UnauthorizedPostAccessException(command.PostId, identity.Id); } @@ -40,7 +42,7 @@ public async Task HandleAsync(UpdatePost command, CancellationToken cancellation throw new UnauthorizedPostOperationException(command.PostId, identity.Id); } - post.Update(command.TextContent, command.MediaContent); + post.Update(command.TextContent, command.MediaContent, _dateTimeProvider.Now); await _postRepository.UpdateAsync(post); await _messageBroker.PublishAsync(new PostUpdated(command.PostId)); 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 index 0773f6d4e..cfce86bad 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs @@ -4,11 +4,12 @@ public class PostDto { public Guid Id { get; set; } public Guid EventId { get; set; } - public Guid StudentId { get; set; } + public Guid OrganizerId { 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; } + public DateTime? UpdatedAt { get; set; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventCreated.cs new file mode 100644 index 000000000..73a98a944 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("events")] + public class EventCreated : IEvent + { + public Guid EventId { get; } + public Guid OrganizerId { get; } + + public EventCreated(Guid eventId, Guid organizerId) + { + EventId = eventId; + OrganizerId = organizerId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventDeleted.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventDeleted.cs new file mode 100644 index 000000000..1281cfba9 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/EventDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("events")] + public class EventDeleted : IEvent + { + public Guid EventId { get; } + + public EventDeleted(Guid eventId) + { + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 000000000..bcbc02e10 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventCreatedHandler.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 EventCreatedHandler : IEventHandler + { + private readonly IEventRepository _eventRepository; + + public EventCreatedHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(EventCreated @event, CancellationToken cancellationToken = default) + { + if (await _eventRepository.ExistsAsync(@event.EventId)) + { + throw new EventAlreadyAddedException(@event.EventId); + } + + await _eventRepository.AddAsync(new Event(@event.EventId, @event.OrganizerId)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventDeletedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventDeletedHandler.cs new file mode 100644 index 000000000..0272f096c --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/EventDeletedHandler.cs @@ -0,0 +1,39 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Commands; +using MiniSpace.Services.Posts.Application.Exceptions; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +{ + public class EventDeletedHandler : IEventHandler + { + private readonly IEventRepository _eventRepository; + private readonly IPostRepository _postRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public EventDeletedHandler(IEventRepository eventRepository, IPostRepository postRepository, + ICommandDispatcher commandDispatcher) + { + _eventRepository = eventRepository; + _postRepository = postRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(EventDeleted @event, CancellationToken cancellationToken = default) + { + if (!(await _eventRepository.ExistsAsync(@event.EventId))) + { + throw new EventNotFoundException(@event.EventId); + } + + var posts = await _postRepository.GetByEventIdAsync(@event.EventId); + foreach (var post in posts) + { + await _commandDispatcher.SendAsync(new DeletePost(post.Id)); + } + + await _eventRepository.DeleteAsync(@event.EventId); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventAlreadyAddedException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventAlreadyAddedException.cs new file mode 100644 index 000000000..ce9ded448 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class EventAlreadyAddedException : AppException + { + public override string Code { get; } = "event_already_added"; + public Guid EventId { get; } + + public EventAlreadyAddedException(Guid eventId) + : base($"Event with id: {eventId} was already added.") + { + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventNotFoundException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventNotFoundException.cs new file mode 100644 index 000000000..dc5eaafb3 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/EventNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class EventNotFoundException : AppException + { + public override string Code { get; } = "event_not_found"; + public Guid Id { get; } + + public EventNotFoundException(Guid id) : base($"Event with id: {id} was not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostCreationAttemptException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostCreationAttemptException.cs new file mode 100644 index 000000000..795313cbc --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/UnauthorizedPostCreationAttemptException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class UnauthorizedPostCreationAttemptException : AppException + { + public override string Code => "unauthorized_post_creation_attempt"; + public Guid OrganizerId { get; } + public Guid EventId { get; } + + public UnauthorizedPostCreationAttemptException(Guid organizerId, Guid eventId) + : base("Unauthorized post creation attempt. Organizer with ID: {OrganizerId} tried to create a post for event with ID: {EventId}.") + { + OrganizerId = organizerId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs new file mode 100644 index 000000000..480b6361c --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetOrganizerPosts.cs @@ -0,0 +1,11 @@ +using System.Collections; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + public class GetOrganizerPosts : IQuery> + { + public Guid OrganizerId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs new file mode 100644 index 000000000..3524d49c6 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetPost.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + public class GetPost : IQuery + { + public Guid PostId { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Event.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Event.cs new file mode 100644 index 000000000..8bd629cb5 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Event.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class Event + { + public Guid Id { get; private set; } + public Guid OrganizerId { get; private set; } + + public Event(Guid id, Guid organizerId) + { + Id = id; + OrganizerId = organizerId; + } + } +} 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 index fe5bbb306..d97c788c3 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs @@ -5,22 +5,24 @@ namespace MiniSpace.Services.Posts.Core.Entities public class Post : AggregateRoot { public Guid EventId { get; private set; } - public Guid StudentId { get; private set; } + public Guid OrganizerId { 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 DateTime? UpdatedAt { get; private set; } - public Post(Guid id, Guid eventId, Guid studentId, string textContent, - string mediaContent, DateTime createdAt, State state, DateTime? publishDate) + public Post(Guid id, Guid eventId, Guid organizerId, string textContent, + string mediaContent, DateTime createdAt, State state, DateTime? publishDate, DateTime? updatedAt = null) { Id = id; EventId = eventId; - StudentId = studentId; + OrganizerId = organizerId; TextContent = textContent; MediaContent = mediaContent; CreatedAt = createdAt; + UpdatedAt = updatedAt; State = state; PublishDate = publishDate; } @@ -30,37 +32,35 @@ public void SetToBePublished(DateTime publishDate, DateTime now) CheckPublishDate(Id, State.ToBePublished, publishDate, now); State = State.ToBePublished; PublishDate = publishDate; + UpdatedAt = now; } - public void SetPublished() + public void SetPublished(DateTime now) { State = State.Published; PublishDate = null; + UpdatedAt = now; } - public void SetInDraft() + public void SetInDraft(DateTime now) { State = State.InDraft; PublishDate = null; + UpdatedAt = now; } - public void SetHidden() - { - State = State.Hidden; - PublishDate = null; - } - - public void SetReported() + public void SetReported(DateTime now) { State = State.Reported; PublishDate = null; + UpdatedAt = now; } public bool UpdateState(DateTime now) { if (State == State.ToBePublished && PublishDate <= now) { - SetPublished(); + SetPublished(now); return true; } @@ -70,24 +70,25 @@ public bool UpdateState(DateTime now) public static Post Create(AggregateId id, Guid eventId, Guid studentId, string textContent, string mediaContent, DateTime createdAt, State state, DateTime? publishDate) { - CheckContent(id, textContent, mediaContent); + CheckTextContent(id, textContent); return new Post(id, eventId, studentId, textContent, mediaContent, createdAt, state, publishDate); } - public void Update(string textContent, string mediaContent) + public void Update(string textContent, string mediaContent, DateTime now) { - CheckContent(Id, textContent, mediaContent); + CheckTextContent(Id, textContent); TextContent = textContent; MediaContent = mediaContent; + UpdatedAt = now; } - private static void CheckContent(AggregateId id, string textContent, string mediaContent) + private static void CheckTextContent(AggregateId id, string textContent) { - if (string.IsNullOrWhiteSpace(textContent) && string.IsNullOrWhiteSpace(mediaContent)) + if (string.IsNullOrWhiteSpace(textContent) || textContent.Length > 5000) { - throw new InvalidPostContentException(id); + throw new InvalidPostTextContentException(id); } } 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 index 816a39ef0..7742012dd 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/State.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/State.cs @@ -5,7 +5,6 @@ public enum State ToBePublished, Published, InDraft, - Hidden, Reported } } 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 deleted file mode 100644 index 4af205297..000000000 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostContentException.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/InvalidPostTextContentException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostTextContentException.cs new file mode 100644 index 000000000..dc6d794ac --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/InvalidPostTextContentException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class InvalidPostTextContentException : DomainException + { + public override string Code { get; } = "invalid_post_text_content"; + public Guid Id { get; } + + public InvalidPostTextContentException(Guid id) : base( + $"Post with id: {id} has invalid content. Its length should be between 1 and 5000 characters.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IEventRepository.cs new file mode 100644 index 000000000..7ce4f9310 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IEventRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IEventRepository + { + Task GetAsync(Guid id); + Task ExistsAsync(Guid id); + Task AddAsync(Event @event); + Task DeleteAsync(Guid id); + } +} 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 index 5029a51a1..cb9a727d7 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs @@ -6,6 +6,7 @@ public interface IPostRepository { Task GetAsync(Guid id); Task> GetToUpdateAsync(); + Task> GetByEventIdAsync(Guid eventId); Task AddAsync(Post post); Task UpdateAsync(Post post); Task DeleteAsync(Guid id); 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 index 06279da18..21453a8b5 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -12,7 +12,7 @@ public object Map(Exception exception, object message) => exception switch { - InvalidPostContentException ex => message switch + InvalidPostTextContentException ex => message switch { CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, ex.Code), @@ -54,7 +54,7 @@ public object Map(Exception exception, object message) ex.State.ToString().ToLowerInvariant(), ex.Message, ex.Code), _ => null, }, - StudentNotFoundException ex => message switch + EventNotFoundException ex => message switch { CreatePost _ => new CreatePostRejected(ex.Id, ex.Message, ex.Code), diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs index bcab93e0e..c5e400ecb 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs @@ -46,7 +46,7 @@ public static class Extensions { public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) { - builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -72,7 +72,7 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddMetrics() .AddJaeger() .AddHandlersLogging() - .AddMongoRepository("students") + .AddMongoRepository("events") .AddMongoRepository("posts") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() @@ -92,9 +92,10 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() - .SubscribeEvent() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent(); return app; } 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 index 806153d20..f060a7f43 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -35,19 +35,19 @@ private static IReadOnlyDictionary MessageTemplates } }, { - typeof(StudentCreated), new HandlerLogTemplate + typeof(EventCreated), new HandlerLogTemplate { - After = "Created a new student with id: {StudentId}." + After = "Created a new event with id: {EventId}." } }, { - typeof(StudentDeleted), new HandlerLogTemplate + typeof(EventDeleted), new HandlerLogTemplate { - After = "Deleted a student with id: {StudentId}." + After = "Deleted an event with id: {EventId}." } }, { - typeof(PostsStateUpdated), + typeof(UpdatePostsState), new HandlerLogTemplate { After = "Updated state of posts at: {Now}." 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/EventDocument.cs similarity index 59% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/StudentDocument.cs rename to MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs index b873cc56c..47b8b3b05 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/StudentDocument.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/EventDocument.cs @@ -2,9 +2,9 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents { - public class StudentDocument : IIdentifiable + public class EventDocument : IIdentifiable { public Guid Id { get; set; } - public string FullName { get; set; } + public Guid OrganizerId { get; set; } } } 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 index 5cb5b168b..86c7f63da 100644 --- 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 @@ -6,18 +6,19 @@ 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); + => new Post(document.Id, document.EventId, document.OrganizerId, document.TextContent, + document.MediaContent, document.CreatedAt, document.State, document.PublishDate, document.UpdatedAt); public static PostDocument AsDocument(this Post entity) => new PostDocument() { Id = entity.Id, EventId = entity.EventId, - StudentId = entity.StudentId, + OrganizerId = entity.OrganizerId, TextContent = entity.TextContent, MediaContent = entity.MediaContent, CreatedAt = entity.CreatedAt, + UpdatedAt = entity.UpdatedAt, State = entity.State, PublishDate = entity.PublishDate }; @@ -27,22 +28,23 @@ public static PostDto AsDto(this PostDocument document) { Id = document.Id, EventId = document.EventId, - StudentId = document.StudentId, + OrganizerId = document.OrganizerId, TextContent = document.TextContent, MediaContent = document.MediaContent, CreatedAt = document.CreatedAt, + UpdatedAt = document.UpdatedAt, State = document.State.ToString().ToLowerInvariant(), PublishDate = document.PublishDate }; - public static Student AsEntity(this StudentDocument document) - => new Student(document.Id, document.FullName); + public static Event AsEntity(this EventDocument document) + => new Event(document.Id, document.OrganizerId); - public static StudentDocument AsDocument(this Student entity) - => new StudentDocument + public static EventDocument AsDocument(this Event entity) + => new EventDocument { Id = entity.Id, - FullName = entity.FullName + OrganizerId = entity.OrganizerId }; } } 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 index efe647ad0..7ca45ee4e 100644 --- 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 @@ -7,11 +7,12 @@ public class PostDocument : IIdentifiable { public Guid Id { get; set; } public Guid EventId { get; set; } - public Guid StudentId { get; set; } + public Guid OrganizerId { 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; } + public DateTime? UpdatedAt { get; set; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs new file mode 100644 index 000000000..4f84c7cb3 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetOrganizerPostsHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Application; +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 GetOrganizerPostsHandler : IQueryHandler> + { + private readonly IMongoRepository _postRepository; + private readonly IAppContext _appContext; + + public GetOrganizerPostsHandler(IMongoRepository postRepository, IAppContext appContext) + { + _postRepository = postRepository; + _appContext = appContext; + } + + public async Task> HandleAsync(GetOrganizerPosts query, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated && identity.Id != query.OrganizerId && !identity.IsAdmin) + { + return Enumerable.Empty(); + } + + var documents = _postRepository.Collection.AsQueryable(); + documents = documents.Where(p => p.OrganizerId == query.OrganizerId); + + var posts = await documents.ToListAsync(); + return posts.Select(p => p.AsDto()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs new file mode 100644 index 000000000..4a6447b53 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs @@ -0,0 +1,25 @@ +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; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers +{ + public class GetPostHandler : IQueryHandler + { + private readonly IMongoRepository _repository; + + public GetPostHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(GetPost query, CancellationToken cancellationToken) + { + var post = await _repository.GetAsync(query.PostId); + + return post?.AsDto(); + } + } +} 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 index 7420c52d6..b75fa383c 100644 --- 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 @@ -2,6 +2,7 @@ using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Application.Dto; using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Core.Entities; using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; using MongoDB.Driver; using MongoDB.Driver.Linq; @@ -20,7 +21,7 @@ public GetPostsHandler(IMongoRepository postRepository) public async Task> HandleAsync(GetPosts query, CancellationToken cancellationToken) { var documents = _postRepository.Collection.AsQueryable(); - documents = documents.Where(p => p.EventId == query.EventId); + documents = documents.Where(p => p.EventId == query.EventId && p.State == State.Published); var posts = await documents.ToListAsync(); return posts.Select(p => p.AsDto()); diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.cs new file mode 100644 index 000000000..687e76e5f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/EventMongoRepository.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 EventMongoRepository : IEventRepository + { + private readonly IMongoRepository _repository; + + public EventMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var @event = await _repository.GetAsync(s => s.Id == id); + + return @event?.AsEntity(); + } + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(s => s.Id == id); + + public Task AddAsync(Event @event) + => _repository.AddAsync(@event.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + } +} 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 index 9a36f751a..7d6985b1d 100644 --- 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 @@ -31,6 +31,13 @@ public async Task> GetToUpdateAsync() return postsToUpdate.Select(e => e.AsEntity()); } + public async Task> GetByEventIdAsync(Guid eventId) + { + var posts = _repository.Collection.AsQueryable(); + var postsByEventId = await posts.Where(e => e.EventId == eventId).ToListAsync(); + return postsByEventId.Select(e => e.AsEntity()); + } + public Task AddAsync(Post post) => _repository.AddAsync(post.AsDocument()); 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 index b469e0263..9ec664240 100644 --- 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 @@ -11,7 +11,7 @@ public class PostStateUpdaterWorker: BackgroundService private readonly IMessageBroker _messageBroker; private readonly ICommandDispatcher _commandDispatcher; private readonly IDateTimeProvider _dateTimeProvider; - private readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(10); + private const int MinutesInterval = 5; public PostStateUpdaterWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, IDateTimeProvider dateTimeProvider) @@ -28,8 +28,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { - await _commandDispatcher.SendAsync(new UpdatePostsState(_dateTimeProvider.Now), stoppingToken); - await Task.Delay(_updateInterval, stoppingToken); + var now = _dateTimeProvider.Now; + var minutes = now.Minute; + if(minutes % MinutesInterval == 0) + { + await _commandDispatcher.SendAsync(new UpdatePostsState(now), stoppingToken); + } + + var nextTime = now.AddMinutes(MinutesInterval - (minutes % MinutesInterval)).AddSeconds(-now.Second) + .AddMilliseconds(-now.Millisecond); + var delay = nextTime - now; + + await Task.Delay(delay, stoppingToken); } catch (TaskCanceledException) { diff --git a/MiniSpace.Services.Posts/src/src.sln b/MiniSpace.Services.Posts/src/src.sln new file mode 100644 index 000000000..4f4c608ce --- /dev/null +++ b/MiniSpace.Services.Posts/src/src.sln @@ -0,0 +1,43 @@ + +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.Posts.Infrastructure", "MiniSpace.Services.Posts.Infrastructure\MiniSpace.Services.Posts.Infrastructure.csproj", "{D7BA9084-525D-4C91-BED3-138F8AF70898}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Posts.Core", "MiniSpace.Services.Posts.Core\MiniSpace.Services.Posts.Core.csproj", "{DB58A343-D580-4C2F-80CF-12A16348290C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Posts.Api", "MiniSpace.Services.Posts.Api\MiniSpace.Services.Posts.Api.csproj", "{244C16A3-6291-4FB8-925A-F1981A0B1EA3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Posts.Application", "MiniSpace.Services.Posts.Application\MiniSpace.Services.Posts.Application.csproj", "{4D79B5C0-6E85-44B6-9BF4-E36822BF711C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D7BA9084-525D-4C91-BED3-138F8AF70898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7BA9084-525D-4C91-BED3-138F8AF70898}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7BA9084-525D-4C91-BED3-138F8AF70898}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7BA9084-525D-4C91-BED3-138F8AF70898}.Release|Any CPU.Build.0 = Release|Any CPU + {DB58A343-D580-4C2F-80CF-12A16348290C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB58A343-D580-4C2F-80CF-12A16348290C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB58A343-D580-4C2F-80CF-12A16348290C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB58A343-D580-4C2F-80CF-12A16348290C}.Release|Any CPU.Build.0 = Release|Any CPU + {244C16A3-6291-4FB8-925A-F1981A0B1EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {244C16A3-6291-4FB8-925A-F1981A0B1EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {244C16A3-6291-4FB8-925A-F1981A0B1EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {244C16A3-6291-4FB8-925A-F1981A0B1EA3}.Release|Any CPU.Build.0 = Release|Any CPU + {4D79B5C0-6E85-44B6-9BF4-E36822BF711C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D79B5C0-6E85-44B6-9BF4-E36822BF711C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D79B5C0-6E85-44B6-9BF4-E36822BF711C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D79B5C0-6E85-44B6-9BF4-E36822BF711C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {866E7205-D441-4C90-BBBD-A17A73430189} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Reactions/.gitignore b/MiniSpace.Services.Reactions/.gitignore new file mode 100644 index 000000000..3cc443daf --- /dev/null +++ b/MiniSpace.Services.Reactions/.gitignore @@ -0,0 +1,333 @@ +## 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 + +.vscode + +# 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.Reactions/Dockerfile b/MiniSpace.Services.Reactions/Dockerfile new file mode 100644 index 000000000..a71aeb5d1 --- /dev/null +++ b/MiniSpace.Services.Reactions/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Reactions.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.Reactions.Api.dll"] diff --git a/MiniSpace.Services.Reactions/LICENSE b/MiniSpace.Services.Reactions/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.Reactions/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.Reactions/MiniSpace.Services.Reactions.sln b/MiniSpace.Services.Reactions/MiniSpace.Services.Reactions.sln new file mode 100644 index 000000000..5823effcb --- /dev/null +++ b/MiniSpace.Services.Reactions/MiniSpace.Services.Reactions.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", "{E78E3850-ECCC-443C-B325-9F13D1639D46}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reactions.Api", "src\MiniSpace.Services.Reactions.Api\MiniSpace.Services.Reactions.Api.csproj", "{05081A01-8BBE-4BB3-931C-4F33F78A7571}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reactions.Application", "src\MiniSpace.Services.Reactions.Application\MiniSpace.Services.Reactions.Application.csproj", "{E122069F-329F-4400-8B3A-057BC704F5FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reactions.Core", "src\MiniSpace.Services.Reactions.Core\MiniSpace.Services.Reactions.Core.csproj", "{6B7F5638-AC31-482B-B806-D7AE537D0A10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Reactions.Infrastructure", "src\MiniSpace.Services.Reactions.Infrastructure\MiniSpace.Services.Reactions.Infrastructure.csproj", "{DC4D92C4-18F6-463B-BD20-C9BEE5F9267F}" +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 + {05081A01-8BBE-4BB3-931C-4F33F78A7571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05081A01-8BBE-4BB3-931C-4F33F78A7571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05081A01-8BBE-4BB3-931C-4F33F78A7571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05081A01-8BBE-4BB3-931C-4F33F78A7571}.Release|Any CPU.Build.0 = Release|Any CPU + {E122069F-329F-4400-8B3A-057BC704F5FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E122069F-329F-4400-8B3A-057BC704F5FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E122069F-329F-4400-8B3A-057BC704F5FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E122069F-329F-4400-8B3A-057BC704F5FC}.Release|Any CPU.Build.0 = Release|Any CPU + {6B7F5638-AC31-482B-B806-D7AE537D0A10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B7F5638-AC31-482B-B806-D7AE537D0A10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B7F5638-AC31-482B-B806-D7AE537D0A10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B7F5638-AC31-482B-B806-D7AE537D0A10}.Release|Any CPU.Build.0 = Release|Any CPU + {DC4D92C4-18F6-463B-BD20-C9BEE5F9267F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC4D92C4-18F6-463B-BD20-C9BEE5F9267F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC4D92C4-18F6-463B-BD20-C9BEE5F9267F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC4D92C4-18F6-463B-BD20-C9BEE5F9267F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {05081A01-8BBE-4BB3-931C-4F33F78A7571} = {E78E3850-ECCC-443C-B325-9F13D1639D46} + {E122069F-329F-4400-8B3A-057BC704F5FC} = {E78E3850-ECCC-443C-B325-9F13D1639D46} + {6B7F5638-AC31-482B-B806-D7AE537D0A10} = {E78E3850-ECCC-443C-B325-9F13D1639D46} + {DC4D92C4-18F6-463B-BD20-C9BEE5F9267F} = {E78E3850-ECCC-443C-B325-9F13D1639D46} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Reactions/scripts/build.sh b/MiniSpace.Services.Reactions/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Reactions/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Reactions/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..5e1dadb65 --- /dev/null +++ b/MiniSpace.Services.Reactions/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.reactions:latest . + +docker tag minispace.services.reactions:latest adrianvsaint/minispace.services.reactions:latest + +docker push adrianvsaint/minispace.services.reactions:latest diff --git a/MiniSpace.Services.Reactions/scripts/start.sh b/MiniSpace.Services.Reactions/scripts/start.sh new file mode 100644 index 000000000..2aae1b7b1 --- /dev/null +++ b/MiniSpace.Services.Reactions/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Reactions.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/scripts/test.sh b/MiniSpace.Services.Reactions/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Reactions/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/MiniSpace.Services.Reactions.Api.csproj b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/MiniSpace.Services.Reactions.Api.csproj new file mode 100644 index 000000000..806f65406 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/MiniSpace.Services.Reactions.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + latest + MiniSpace.Services.Reactions.Api + disable + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs new file mode 100644 index 000000000..a1961adcf --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.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.Reactions.Application; +using MiniSpace.Services.Reactions.Application.Commands; +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Application.Queries; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Infrastructure; + +namespace MiniSpace.Services.Reactions.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>("reactions") + .Get("reactions/summary") + .Post("reactions", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"reactions/{cmd.ReactionId}")) + .Delete("reactions/{reactionId}") + )) + .UseLogging() + .Build() + .RunAsync(); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Properties/launchSettings.json b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Properties/launchSettings.json new file mode 100644 index 000000000..94b93ab62 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5010" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Reactions": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.Development.json b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.Development.json new file mode 100644 index 000000000..f3ee419db --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.docker.json b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.docker.json new file mode 100644 index 000000000..ee0ec548a --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Reactions Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "reactions-service", + "address": "reactions-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "reactions-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": "reactions", + "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": "reactions-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "reactions-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": "reactions" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "reactions-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "reactions:" + }, + "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.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.json b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.json new file mode 100644 index 000000000..4d566948d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.local.json b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.local.json new file mode 100644 index 000000000..be6106839 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/appsettings.local.json @@ -0,0 +1,199 @@ +{ + "app": { + "name": "MiniSpace Reactions Service", + "service": "reactions-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "reactions-service", + "address": "docker.for.win.localhost", + "port": "5010", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "reactions-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": "reactions", + "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": "reactions-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "reactions-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": "reactions" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "reactions-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "reactions:" + }, + "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": "reactions-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "reactions-service", + "commonName": "reactions-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "reactions-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/CreateReaction.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/CreateReaction.cs new file mode 100644 index 000000000..258ccca21 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/CreateReaction.cs @@ -0,0 +1,27 @@ +using System.Data; +using System.Threading.Tasks.Dataflow; +using Convey.CQRS.Commands; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Commands +{ + public class CreateReaction : ICommand + { + public Guid ReactionId {get;} + public Guid StudentId { get; } + public string ReactionType { get; } + public Guid ContentId {get;} + public string ContentType { get; } + + public CreateReaction(Guid reactionId, Guid studentId, + Guid contentId, + string reactionType, string contentType) + { + ReactionId = reactionId == Guid.Empty ? Guid.NewGuid() : reactionId; + StudentId = studentId; + ContentId = contentId; + ReactionType = reactionType; + ContentType = contentType; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/DeleteReaction.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/DeleteReaction.cs new file mode 100644 index 000000000..79d3c8297 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/DeleteReaction.cs @@ -0,0 +1,16 @@ +using System.Reflection.Metadata; +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Commands +{ + public class DeleteReaction : ICommand + { + public Guid ReactionId { get; } + public DeleteReaction(Guid reactionId) + { + ReactionId = reactionId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs new file mode 100644 index 000000000..65dde4920 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs @@ -0,0 +1,81 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reactions.Application.Events; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Application.Services; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Exceptions; +using MiniSpace.Services.Reactions.Core.Repositories; + +namespace MiniSpace.Services.Reactions.Application.Commands.Handlers +{ + public class CreateReactionHandler(IReactionRepository reactionRepository, + IPostRepository postRepository, + IEventRepository eventRepository, + IStudentRepository studentRepository, + IAppContext appContext, + IMessageBroker messageBroker + ) : ICommandHandler + { + private readonly IReactionRepository _reactionRepository = reactionRepository; + private readonly IPostRepository _postRepository = postRepository; + private readonly IStudentRepository _studentRepository = studentRepository; + private readonly IEventRepository _eventRepository = eventRepository; + private readonly IMessageBroker _messageBroker = messageBroker; + private readonly IAppContext _appContext = appContext; + + public async Task HandleAsync(CreateReaction command, CancellationToken cancellationToken = default) + { + var identity = _appContext.Identity; + + if (identity.IsAuthenticated && identity.Id != command.StudentId) { + throw new UnauthorizedReactionAccessException(command.ReactionId, command.StudentId); + } + + if (!await _studentRepository.ExistsAsync(command.StudentId)) + { + throw new StudentNotFoundException(command.StudentId); + } + + // Check the content type + if (!Enum.TryParse(command.ContentType, true, out var contentType)) { + throw new InvalidReactionContentTypeException(command.ContentType); + } + + // Check the content + switch (contentType) { + case ReactionContentType.Event: + if (!await _eventRepository.ExistsAsync(command.ContentId)) { + throw new EventNotFoundException(command.ContentId); + } + break; + case ReactionContentType.Post: + if (!await _postRepository.ExistsAsync(command.ContentId)) { + throw new PostNotFoundException(command.ContentId); + } + break; + default: + break; + } + + // check the reaction type + // case-sensitive + if (!Enum.TryParse(command.ReactionType, false, out var reactionType)) + { + throw new InvalidReactionTypeException(command.ReactionType); + } + + if (await _reactionRepository.ExistsAsync(command.ContentId, contentType, command.StudentId)) + { + throw new StudentAlreadyGaveReactionException(command.StudentId, command.ContentId, contentType); + } + + string studentFullName = identity.Name; + + var reaction = Reaction.Create(command.ReactionId, command.StudentId, studentFullName, reactionType, + command.ContentId, contentType); + await _reactionRepository.AddAsync(reaction); + + await _messageBroker.PublishAsync(new ReactionCreated(command.ReactionId)); + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/DeleteReactionHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/DeleteReactionHandler.cs new file mode 100644 index 000000000..c80074cc2 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/DeleteReactionHandler.cs @@ -0,0 +1,33 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reactions.Application.Events; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Application.Services; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Exceptions; +using MiniSpace.Services.Reactions.Core.Repositories; + +namespace MiniSpace.Services.Reactions.Application.Commands.Handlers +{ + public class DeleteReactionHandler(IReactionRepository reactionRepository, + IAppContext appContext, + IMessageBroker messageBroker) : ICommandHandler + { + private readonly IReactionRepository _reactionRepository = reactionRepository; + private readonly IMessageBroker _messageBroker = messageBroker; + private readonly IAppContext _appContext = appContext; + public async Task HandleAsync(DeleteReaction command, CancellationToken cancellationToken = default) + { + var reaction = await _reactionRepository.GetAsync(command.ReactionId) ?? + throw new ReactionNotFoundException(command.ReactionId); + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != reaction.StudentId) + { + throw new UnauthorizedReactionAccessException(command.ReactionId, identity.Id); + } + + await _reactionRepository.DeleteAsync(command.ReactionId); + await _messageBroker.PublishAsync(new ReactionDeleted(command.ReactionId)); + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/ContractAttribute.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/ContractAttribute.cs new file mode 100644 index 000000000..8a66ff792 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reactions.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionDto.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionDto.cs new file mode 100644 index 000000000..474c1478f --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionDto.cs @@ -0,0 +1,15 @@ + +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Dto +{ + public class ReactionDto + { + public Guid Id { get; set; } + public Guid StudentId {get;set;} + public string StudentFullName {get;set;} + public Guid ContentId{get;set;} + public ReactionContentType ContentType{get;set;} + public ReactionType Type {get;set;} + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs new file mode 100644 index 000000000..42adb1df7 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs @@ -0,0 +1,14 @@ + +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Dto +{ + public class ReactionsSummaryDto(int nrReactions, ReactionType? dominant, + Guid? authUserReactionId, ReactionType? authUserReactionType) + { + public int NumberOfReactions {get;set;} = nrReactions; + public ReactionType? DominantReaction {get;set;} = dominant; + public Guid? AuthUserReactionId {get;set;} = authUserReactionId; + public ReactionType? AuthUserReactionType {get;set;} = authUserReactionType; + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/EventCreated.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/EventCreated.cs new file mode 100644 index 000000000..899f938e2 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/EventCreated.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reactions.Application.Events.External +{ + [Message("events")] + public class EventCreated : IEvent + { + public Guid EventId { get; } + public Guid OrganizerId { get; } + public EventCreated(Guid eventId, Guid organizerId) + { + EventId = eventId; + OrganizerId = organizerId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 000000000..055abcd48 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/EventCreatedHandler.cs @@ -0,0 +1,27 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; + +namespace MiniSpace.Services.Reactions.Application.Events.External.Handlers +{ + public class EventCreatedHandler : IEventHandler + { + private readonly IEventRepository _eventRepository; + + public EventCreatedHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(EventCreated @event, CancellationToken cancellationToken = default) + { + if (await _eventRepository.ExistsAsync(@event.EventId)) + { + throw new EventAlreadyAddedException(@event.EventId); + } + + await _eventRepository.AddAsync(new Event(@event.EventId)); + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/PostCreatedHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/PostCreatedHandler.cs new file mode 100644 index 000000000..9a146fdb5 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/PostCreatedHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; + +namespace MiniSpace.Services.Reactions.Application.Events.External.Handlers +{ + public class PostCreatedHandler : IEventHandler + { + private readonly IPostRepository _postRepository; + + public PostCreatedHandler(IPostRepository postRepository) + { + _postRepository = postRepository; + } + + public async Task HandleAsync(PostCreated @event, CancellationToken cancellationToken) + { + if (await _postRepository.ExistsAsync(@event.PostId)) + { + throw new StudentAlreadyAddedException(@event.PostId); + } + + await _postRepository.AddAsync(new Post(@event.PostId)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/StudentCreatedHandler.cs similarity index 63% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs rename to MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/StudentCreatedHandler.cs index 4d3252933..82e65eb76 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/StudentCreatedHandler.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -1,9 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; using Convey.CQRS.Events; -using MiniSpace.Services.Posts.Application.Exceptions; -using MiniSpace.Services.Posts.Core.Entities; -using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; -namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +namespace MiniSpace.Services.Reactions.Application.Events.External.Handlers { public class StudentCreatedHandler : IEventHandler { @@ -14,14 +17,14 @@ public StudentCreatedHandler(IStudentRepository studentRepository) _studentRepository = studentRepository; } - public async Task HandleAsync(StudentCreated @event, CancellationToken cancellationToken = default) + 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, @event.FullName)); + + await _studentRepository.AddAsync(new Student(@event.StudentId)); } - } -} + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/PostCreated.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/PostCreated.cs new file mode 100644 index 000000000..605fdf17c --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/PostCreated.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + + +namespace MiniSpace.Services.Reactions.Application.Events +{ + [Message("posts")] + public class PostCreated : IEvent + { + public Guid PostId { get; } + + public PostCreated(Guid postId) + { + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/StudentCreated.cs new file mode 100644 index 000000000..6d086acdc --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/External/StudentCreated.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs new file mode 100644 index 000000000..c3b5031a9 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs @@ -0,0 +1,16 @@ +using System.Net.Mime; +using Convey.CQRS.Events; +using Microsoft.AspNetCore.Connections; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Events +{ + public class ReactionCreated : IEvent + { + public Guid ReactionId {get;set;} + public ReactionCreated(Guid reactionId) + { + ReactionId=reactionId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionDeleted.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionDeleted.cs new file mode 100644 index 000000000..23358e8d3 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionDeleted.cs @@ -0,0 +1,16 @@ +using System.Net.Mime; +using Convey.CQRS.Events; +using Microsoft.AspNetCore.Connections; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Events +{ + public class ReactionDeleted : IEvent + { + public Guid ReactionId {get;set;} + public ReactionDeleted(Guid reactionId) + { + ReactionId = reactionId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/AddReactionRejected.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/AddReactionRejected.cs new file mode 100644 index 000000000..d6bc68e8f --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/AddReactionRejected.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Events; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Events.Rejected +{ + public class AddReactionRejected(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.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/DeleteReactionRejected.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/DeleteReactionRejected.cs new file mode 100644 index 000000000..94fc7c70b --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/Rejected/DeleteReactionRejected.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Events; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Events.Rejected +{ + public class DeleteReactionRejected(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.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/AppException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..c35340ae3 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventAlreadyAddedException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventAlreadyAddedException.cs new file mode 100644 index 000000000..89ba15fb4 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventAlreadyAddedException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class EventAlreadyAddedException : AppException + { + public override string Code { get; } = "event_already_added"; + public Guid EventId { get; } + + public EventAlreadyAddedException(Guid eventId) + : base($"Event with id: {eventId} was already added.") + { + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventNotFoundException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventNotFoundException.cs new file mode 100644 index 000000000..401b17cb2 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/EventNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/PostNotFoundException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/PostNotFoundException.cs new file mode 100644 index 000000000..d4de8aa38 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/PostNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class PostNotFoundException : AppException + { + public override string Code { get; } = "post_not_found"; + public Guid PostId { get; } + + public PostNotFoundException(Guid postId) : base($"Post with ID: '{postId}' was not found.") + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/ReactionNotFound.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/ReactionNotFound.cs new file mode 100644 index 000000000..9f7f3d3df --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/ReactionNotFound.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class ReactionNotFoundException : AppException + { + public override string Code { get; } = "reaction_not_found"; + public Guid ReactionId; + + public ReactionNotFoundException(Guid reactionId) : + base($"Reaction with id: {reactionId} was not found.") + { + ReactionId = reactionId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyAddedException.cs similarity index 86% rename from MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs rename to MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyAddedException.cs index af328da1b..538b03aab 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/StudentAlreadyAddedException.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyAddedException.cs @@ -1,4 +1,4 @@ -namespace MiniSpace.Services.Posts.Application.Exceptions +namespace MiniSpace.Services.Reactions.Application.Exceptions { public class StudentAlreadyAddedException : AppException { diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyGaveReactionException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyGaveReactionException.cs new file mode 100644 index 000000000..ab2137644 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentAlreadyGaveReactionException.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class StudentAlreadyGaveReactionException : AppException + { + public Guid StudentId { get; } + public Guid ContentId { get; } + public ReactionContentType ContentType { get; } + + public StudentAlreadyGaveReactionException(Guid studentId, Guid contentId, ReactionContentType contentType) + : base($"Student with id: '{studentId}' already gave a reaction to the content with id: '{contentId}' and content type: '{contentType}'.") + { + StudentId = studentId; + ContentId = contentId; + ContentType = contentType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentNotFoundException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentNotFoundException.cs new file mode 100644 index 000000000..a6a2f303e --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/StudentNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/UnauthorizedReactionAccessException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/UnauthorizedReactionAccessException.cs new file mode 100644 index 000000000..1657aecf0 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Exceptions/UnauthorizedReactionAccessException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Reactions.Application.Exceptions +{ + public class UnauthorizedReactionAccessException : AppException + { + public override string Code { get; } = "unauthorized_reaction_access"; + public Guid UserId { get; } + public Guid ReactionId {get;} + + public UnauthorizedReactionAccessException(Guid reactionId, Guid userId) : + base($"Unauthorized access to reaction with id: '{reactionId}' by user with id: '{userId}'.") + { + ReactionId = reactionId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Extensions.cs new file mode 100644 index 000000000..eedc5326f --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reactions.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IAppContext.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IAppContext.cs new file mode 100644 index 000000000..e93f60139 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Reactions.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IIdentityContext.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IIdentityContext.cs new file mode 100644 index 000000000..09394f8d9 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Application/MiniSpace.Services.Reactions.Application.csproj b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/MiniSpace.Services.Reactions.Application.csproj new file mode 100644 index 000000000..655099070 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/MiniSpace.Services.Reactions.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactions.cs new file mode 100644 index 000000000..35da121ab --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactions.cs @@ -0,0 +1,13 @@ + +using Convey.CQRS.Queries; +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Application.Queries +{ + public class GetReactions : IQuery> + { + public Guid ContentId { get; set; } + public ReactionContentType ContentType { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactionsSummary.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactionsSummary.cs new file mode 100644 index 000000000..da876e974 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Queries/GetReactionsSummary.cs @@ -0,0 +1,14 @@ + +using Convey.CQRS.Queries; +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.Application.Queries +{ + public class GetReactionsSummary : IQuery + { + public Guid ContentId { get; set; } + public ReactionContentType ContentType { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..aae776a71 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reactions.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IEventMapper.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..65cbe1c90 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reactions.Core.Events; + +namespace MiniSpace.Services.Reactions.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..dc95763b7 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Reactions.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateId.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..2ddc1effb --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..0a5326133 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Reactions.Core.Events; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ContentType.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ContentType.cs new file mode 100644 index 000000000..72dc6249a --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ContentType.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public enum ReactionContentType + { + Event, + Post + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Event.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Event.cs new file mode 100644 index 000000000..c9fdd4339 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Event.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public class Event + { + public Guid Id { get; private set; } + + public Event(Guid id) + { + Id = id; + } + + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Post.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Post.cs new file mode 100644 index 000000000..b0ba20b4f --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Post.cs @@ -0,0 +1,9 @@ + + +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public class Post(Guid id) + { + public Guid Id { get; private set; } = id; + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Reaction.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Reaction.cs new file mode 100644 index 000000000..c177ce101 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Reaction.cs @@ -0,0 +1,34 @@ +using System.Net.Mime; +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public class Reaction : AggregateRoot + { + public Guid StudentId { get; private set; } + public string StudentFullName {get;private set;} + public ReactionType ReactionType { get; private set; } + public Guid ContentId { get; private set; } + public ReactionContentType ContentType { get; private set; } + + public Reaction(Guid reactionId, Guid studentId, string studentFullName, ReactionType reactionType, + Guid contentId, ReactionContentType contentType) + { + + Id = reactionId; + StudentId = studentId; + StudentFullName = studentFullName; + ReactionType = reactionType; + ContentId = contentId; + ContentType = contentType; + } + + public static Reaction Create(Guid reactionId, Guid studentId, string studentFullName, ReactionType reactionType, + Guid contentId, ReactionContentType contentType) + { + return new Reaction(reactionId, studentId, studentFullName, reactionType, contentId, contentType); + } + + + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ReactionType.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ReactionType.cs new file mode 100644 index 000000000..8d96e5850 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/ReactionType.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public enum ReactionType + { + LoveIt, + LikeIt, + Wow, + ItWasOkay, + HateIt + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Student.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Student.cs new file mode 100644 index 000000000..7d7140347 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Entities/Student.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Reactions.Core.Entities +{ + public class Student + { + public Guid Id { get; private set; } + + public Student(Guid id) + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..8825fe8cd --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reactions.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..8a26b1d91 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/DomainException.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Reactions.Core.Exceptions +{ + public abstract class DomainException(string message) : Exception(message) + { + public virtual string Code { get; } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..4b0d05ab0 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionContentTypeException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionContentTypeException.cs new file mode 100644 index 000000000..ac9b64e13 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionContentTypeException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reactions.Core.Exceptions +{ + public class InvalidReactionContentTypeException : DomainException + { + public override string Code { get; } = "invalid_post_state"; + public string InvalidReactionContentType { get; } + + public InvalidReactionContentTypeException(string invalidReactionContentType) : base( + $"String: {invalidReactionContentType} cannot be parsed to valid reaction content type.") + { + InvalidReactionContentType = invalidReactionContentType; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionTypeException.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionTypeException.cs new file mode 100644 index 000000000..c15dfd781 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Exceptions/InvalidReactionTypeException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Reactions.Core.Exceptions +{ + public class InvalidReactionTypeException : DomainException + { + public override string Code { get; } = "invalid_post_state"; + public string InvalidReactionType { get; } + + public InvalidReactionTypeException(string invalidReactionType) : base( + $"String: {invalidReactionType} cannot be parsed to valid reaction type.") + { + InvalidReactionType = invalidReactionType; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/MiniSpace.Services.Reactions.Core.csproj b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/MiniSpace.Services.Reactions.Core.csproj new file mode 100644 index 000000000..de191f507 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/MiniSpace.Services.Reactions.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IEventRepository.cs new file mode 100644 index 000000000..dcd8fe368 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IEventRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Core.Repositories +{ + public interface IEventRepository + { + Task ExistsAsync(Guid id); + Task AddAsync(Event @event); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IPostRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IPostRepository.cs new file mode 100644 index 000000000..780df8001 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IPostRepository.cs @@ -0,0 +1,10 @@ +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Core.Repositories +{ + public interface IPostRepository + { + Task ExistsAsync(Guid id); + Task AddAsync(Post post); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs new file mode 100644 index 000000000..6c995d83a --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs @@ -0,0 +1,15 @@ +using System.Formats.Tar; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Core.Repositories +{ + public interface IReactionRepository + { + Task> GetReactionsAsync(Guid contentId, ReactionContentType contentType); + Task GetAsync(Guid id); + Task AddAsync(Reaction reaction); + Task DeleteAsync(Guid id); + Task ExistsAsync(Guid contentId, ReactionContentType contentType, Guid studentId); + + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IStudentRepository.cs new file mode 100644 index 000000000..d487b277f --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IStudentRepository.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..ce174891c --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Reactions.Application; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..558fa1860 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Reactions.Application; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..992fd7079 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..073e96468 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.Reactions.Application; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..dd770fd56 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.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.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..57bd45159 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.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.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..21e9c097c --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,23 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Reactions.Application.Commands; +using MiniSpace.Services.Reactions.Application.Events.Rejected; + +//using MiniSpace.Services.Reactions.Application.Events.Rejected; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + InvalidReactionTypeException ex => new AddReactionRejected(Guid.Empty, ex.Message, ex.Code), + InvalidAggregateIdException ex => new AddReactionRejected(Guid.Empty, ex.Message, ex.Code), + InvalidReactionContentTypeException ex => new AddReactionRejected(Guid.Empty, ex.Message, ex.Code), + EventNotFoundException ex => new AddReactionRejected(Guid.Empty, ex.Message, ex.Code), + _ => null, + }; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..213b0e342 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs new file mode 100644 index 000000000..7de580200 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs @@ -0,0 +1,146 @@ +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.Reactions.Application; +using MiniSpace.Services.Reactions.Application.Commands; +using MiniSpace.Services.Reactions.Application.Services; +using MiniSpace.Services.Reactions.Core.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Contexts; +using MiniSpace.Services.Reactions.Infrastructure.Decorators; +using MiniSpace.Services.Reactions.Infrastructure.Exceptions; +using MiniSpace.Services.Reactions.Infrastructure.Logging; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Services; +using MiniSpace.Services.Reactions.Application.Queries; +using Convey.Logging.CQRS; +using MiniSpace.Services.Reactions.Application.Events; +using MiniSpace.Services.Reactions.Application.Events.External; + +namespace MiniSpace.Services.Reactions.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + // add repositories + builder.Services.AddTransient(); + builder.Services.AddTransient(); + 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<>)); + + // background workers: none + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("reactions") + .AddMongoRepository("posts") + .AddMongoRepository("events") + .AddMongoRepository("students") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..2a761c840 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Reactions.Application; + +namespace MiniSpace.Services.Reactions.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..e642cb90d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,26 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Reactions.Application.Commands; + +namespace MiniSpace.Services.Reactions.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + // TODO: Posts had only UpdatePost + var assemblyCreate = typeof(CreateReaction).Assembly; + var assemblyDelete = typeof(DeleteReaction).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assemblyCreate) + .AddCommandHandlersLogging(assemblyDelete) + .AddEventHandlersLogging(assemblyCreate) + .AddEventHandlersLogging(assemblyDelete) + ; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..a2383e89c --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,51 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.Reactions.Application.Commands; +using MiniSpace.Services.Reactions.Application.Events; +using MiniSpace.Services.Reactions.Application.Events.External; + +namespace MiniSpace.Services.Reactions.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(CreateReaction), new HandlerLogTemplate + { + After = "Created the reaction with id: {ReactionId}." + } + }, + { + typeof(DeleteReaction), new HandlerLogTemplate + { + After = "Delete the reaction with id: {ReactionId}." + } + }, + { + typeof(EventCreated), new HandlerLogTemplate + { + After = "Created a new event with id: {EventId}." + } + }, + { + typeof(PostCreated), new HandlerLogTemplate + { + After = "Created a new post with id: {PostId}." + } + }, + { + typeof(StudentCreated), new HandlerLogTemplate + { + After = "Created a new student with id: {StudentId}." + } + }, + }; + + 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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/MiniSpace.Services.Reactions.Infrastructure.csproj b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/MiniSpace.Services.Reactions.Infrastructure.csproj new file mode 100644 index 000000000..c81a53625 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/MiniSpace.Services.Reactions.Infrastructure.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/EventDocument.cs new file mode 100644 index 000000000..d17d51cbc --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/EventDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents +{ + public class EventDocument : IIdentifiable + { + public Guid Id { get; set; } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..df263b75d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,56 @@ +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static Reaction AsEntity(this ReactionDocument document) + => new Reaction(document.Id, document.StudentId, document.StudentFullName, document.Type, + document.ContentId, document.ContentType); + + public static ReactionDocument AsDocument(this Reaction entity) + => new ReactionDocument() + { + Id = entity.Id, + StudentId = entity.StudentId, + StudentFullName = entity.StudentFullName, + Type = entity.ReactionType, + ContentId = entity.ContentId, + ContentType = entity.ContentType, + + }; + + public static ReactionDto AsDto(this ReactionDocument document) + => new ReactionDto() + { + Id = document.Id, + StudentId = document.StudentId, + StudentFullName = document.StudentFullName, + Type = document.Type, + ContentId = document.ContentId, + ContentType = document.ContentType + }; + + public static Student AsEntity(this StudentDocument document) + => new Student(document.Id); + + public static StudentDocument AsDocument(this Student entity) + => new StudentDocument + { + Id = entity.Id + }; + + public static EventDocument AsDocument(this Event entity) + => new EventDocument + { + Id = entity.Id + }; + + public static PostDocument AsDocument(this Post entity) + => new PostDocument + { + Id = entity.Id + }; + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/PostDocument.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/PostDocument.cs new file mode 100644 index 000000000..7667f01d5 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/PostDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents +{ + public class PostDocument : IIdentifiable + { + public Guid Id { get; set; } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs new file mode 100644 index 000000000..70839cd63 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs @@ -0,0 +1,16 @@ +using Convey.Types; +using Convey.WebApi.CQRS; +using MiniSpace.Services.Reactions.Core.Entities; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents +{ + public class ReactionDocument : IIdentifiable + { + public Guid Id {get;set;} + public Guid StudentId {get;set;} + public string StudentFullName {get;set;} + public Guid ContentId{get;set;} + public ReactionContentType ContentType{get;set;} + public ReactionType Type {get;set;} + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/StudentDocument.cs new file mode 100644 index 000000000..045f88352 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -0,0 +1,9 @@ +using Convey.Types; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents +{ + public class StudentDocument : IIdentifiable + { + public Guid Id { get; set; } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs new file mode 100644 index 000000000..d488e6a81 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using DnsClient; +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Application.Queries; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Queries.Handlers +{ + public class GetReactionsHandler : IQueryHandler> + { + private readonly IMongoRepository _reactionRepository; + + public GetReactionsHandler(IMongoRepository reactionRepository) + { + _reactionRepository = reactionRepository; + } + + public async Task> HandleAsync(GetReactions query, CancellationToken cancellationToken) + { + var documents = _reactionRepository.Collection.AsQueryable(); + documents = documents.Where(p => p.ContentId == query.ContentId && p.ContentType == query.ContentType); + + var reactions = await documents.ToListAsync(); + return reactions.Select(p => p.AsDto()); + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs new file mode 100644 index 000000000..62da6a09d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs @@ -0,0 +1,53 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reactions.Application; +using MiniSpace.Services.Reactions.Application.Dto; +using MiniSpace.Services.Reactions.Application.Queries; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Queries.Handlers +{ + public class GetReactionsSummaryHandler : IQueryHandler + { + private readonly IMongoRepository _reactionRepository; + + private readonly IAppContext _appContext; + + public GetReactionsSummaryHandler(IMongoRepository reactionRepository, IAppContext appContext) + { + _reactionRepository = reactionRepository; + _appContext = appContext; + } + + public async Task + HandleAsync(GetReactionsSummary query, CancellationToken cancellationToken) + { + var documents = _reactionRepository.Collection.AsQueryable(); + documents = documents.Where(p => p.ContentId == query.ContentId && p.ContentType == query.ContentType); + + var reactions = await documents.ToListAsync(); + int nrReactions = reactions.Count; + + if (nrReactions == 0) { + return new ReactionsSummaryDto(0, null, null, null); + } + + var identity = _appContext.Identity; + Guid? authUserReactionId = null; + ReactionType? authUserReactionType = null; + + if (identity.IsAuthenticated && reactions.Exists(x => x.StudentId == identity.Id)) { + var reactionDocument = reactions.Find(x => x.StudentId == identity.Id); + authUserReactionId = reactionDocument.Id; + authUserReactionType = reactionDocument.Type; + } + + ReactionType dominant = reactions.GroupBy(x => x.Type) + .OrderBy(x => x.ToList().Count).Last().Key; + return new ReactionsSummaryDto(nrReactions, dominant, authUserReactionId, authUserReactionType); + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/EventMongoRepository.cs new file mode 100644 index 000000000..e33b1fce9 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -0,0 +1,23 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Repositories +{ + public class EventMongoRepository : IEventRepository + { + private readonly IMongoRepository _repository; + + public EventMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public Task AddAsync(Event @event) + => _repository.AddAsync(@event.AsDocument()); + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(s => s.Id == id); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/PostMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/PostMongoRepository.cs new file mode 100644 index 000000000..85506560d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/PostMongoRepository.cs @@ -0,0 +1,25 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Repositories +{ + public class PostMongoRepository : IPostRepository + { + private readonly IMongoRepository _repository; + + public PostMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public Task AddAsync(Post post) + { + return _repository.AddAsync(post.AsDocument()); + } + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(s => s.Id == id); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs new file mode 100644 index 000000000..8721c6c3d --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs @@ -0,0 +1,43 @@ +using Convey.Persistence.MongoDB; +using Jaeger.Propagation; +using Microsoft.AspNetCore.Components.Forms; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Repositories +{ + public class ReactionMongoRepository(IMongoRepository repository) : IReactionRepository + { + private readonly IMongoRepository _repository = repository; + + public async Task> GetReactionsAsync(Guid contentId, ReactionContentType contentType) + { + var reactions = _repository.Collection.AsQueryable(); + var reactionsAsync = await reactions.ToListAsync(); + return reactionsAsync.Select(x => x.AsEntity()); + } + + public Task AddAsync(Reaction reaction) + { + return _repository.AddAsync(reaction.AsDocument()); + } + + public async Task GetAsync(Guid id) + { + var reaction = await _repository.GetAsync(x => x.Id == id); + return reaction?.AsEntity(); + } + + public Task DeleteAsync(Guid id) + { + return _repository.DeleteAsync(x => x.Id == id); + } + + public Task ExistsAsync(Guid contentId, ReactionContentType contentType, Guid studentId) + => _repository.ExistsAsync(x => x.ContentId == contentId && x.ContentType == contentType + && x.StudentId == studentId); + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs new file mode 100644 index 000000000..1b0bfaddd --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/StudentMongoRepository.cs @@ -0,0 +1,40 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; +using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Reactions.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 AddAsync(Student student) + // { + // throw new NotImplementedException(); + // } + + // public Task DeleteAsync(Guid id) + // { + // throw new NotImplementedException(); + // } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..29b0af768 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Reactions.Application.Services; + +namespace MiniSpace.Services.Reactions.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..c68dd37f6 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Reactions.Application.Services; +using MiniSpace.Services.Reactions.Core; +using MiniSpace.Services.Reactions.Core.Events; + +namespace MiniSpace.Services.Reactions.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.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..ae7b64798 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.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.Reactions.Application.Services; + +namespace MiniSpace.Services.Reactions.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.Students/src/MiniSpace.Services.Students.Api/Program.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs index 38d3b7d0d..d32e63650 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Convey; +using Convey.CQRS.Queries; using Convey.Logging; using Convey.Types; using Convey.WebApi; @@ -31,7 +32,8 @@ public static async Task Main(string[] args) .UseInfrastructure() .UseDispatcherEndpoints(endpoints => endpoints .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) - .Get>("students") + // .Get>("students") + .Get>("students") .Get("students/{studentId}") .Put("students/{studentId}") .Delete("students/{studentId}") diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.sln b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.sln new file mode 100644 index 000000000..7be171b41 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/MiniSpace.Services.Students.Application.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.Application", "MiniSpace.Services.Students.Application.csproj", "{D40458B1-1641-441A-8B03-14D113FF10B1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D40458B1-1641-441A-8B03-14D113FF10B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D40458B1-1641-441A-8B03-14D113FF10B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D40458B1-1641-441A-8B03-14D113FF10B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D40458B1-1641-441A-8B03-14D113FF10B1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {13CB57E2-9B33-4F5F-8688-CE9DFF796CAE} + EndGlobalSection +EndGlobal 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 index 611b134b0..abc3cf829 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudents.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudents.cs @@ -3,7 +3,14 @@ namespace MiniSpace.Services.Students.Application.Queries { - public class GetStudents : IQuery> + public class GetStudents : IQuery>, IPagedGetStudentsQuery { + public int Page { get; set; } + public int ResultsPerPage { get; set; } + + public string OrderBy { get; set; } + public string SortOrder { get; set; } + + public int Results { get; set; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/IPagedGetStudentsQuery.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/IPagedGetStudentsQuery.cs new file mode 100644 index 000000000..4ed7fdc5e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/IPagedGetStudentsQuery.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Queries; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public interface IPagedGetStudentsQuery : Convey.CQRS.Queries.IPagedQuery + { + new int Page { get; set; } + new int ResultsPerPage { get; set; } + new string OrderBy { get; set; } + new string SortOrder { get; set; } + int Results { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/PagedResult.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/PagedResult.cs new file mode 100644 index 000000000..5b8bbbf48 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/PagedResult.cs @@ -0,0 +1,28 @@ +using Convey.CQRS.Queries; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class PagedResult + { + public List Results { get; set; } + public int Total { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + public string NextPage { get; set; } + public string PrevPage { get; set; } + + public PagedResult(List results, int total, int pageSize, int page, string baseUrl) + { + Results = results; + Total = total; + PageSize = pageSize; + Page = page; + + // Calculate if there's a next page + int totalPages = (int)Math.Ceiling(total / (double)pageSize); + NextPage = page < totalPages ? $"{baseUrl}?page={page + 1}&resultsPerPage={pageSize}" : null; + PrevPage = page > 1 ? $"{baseUrl}?page={page - 1}&resultsPerPage={pageSize}" : null; + } + } + +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.sln b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.sln new file mode 100644 index 000000000..ec5871591 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/MiniSpace.Services.Students.Core.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.Core", "MiniSpace.Services.Students.Core.csproj", "{5FCCF940-AF95-4A87-8430-DB87874ECD32}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5FCCF940-AF95-4A87-8430-DB87874ECD32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FCCF940-AF95-4A87-8430-DB87874ECD32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FCCF940-AF95-4A87-8430-DB87874ECD32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FCCF940-AF95-4A87-8430-DB87874ECD32}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66258119-6109-4543-BC02-03F9BDE534C7} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.sln b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.sln new file mode 100644 index 000000000..46548eb2c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/MiniSpace.Services.Students.Infrastructure.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.Infrastructure", "MiniSpace.Services.Students.Infrastructure.csproj", "{10D8AAB6-B50A-491D-834D-F686917B9BF4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10D8AAB6-B50A-491D-834D-F686917B9BF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10D8AAB6-B50A-491D-834D-F686917B9BF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10D8AAB6-B50A-491D-834D-F686917B9BF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10D8AAB6-B50A-491D-834D-F686917B9BF4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {27BD54BB-13EA-4A4E-889B-37FC528BA22B} + EndGlobalSection +EndGlobal 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 index b90d07fdd..654a60129 100644 --- 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 @@ -3,23 +3,39 @@ using MiniSpace.Services.Students.Application.Dto; using MiniSpace.Services.Students.Application.Queries; using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers { - public class GetStudentsHandler : IQueryHandler> + public class GetStudentsHandler : IQueryHandler> { private readonly IMongoRepository _studentRepository; + private const string BaseUrl = "students"; public GetStudentsHandler(IMongoRepository studentRepository) { _studentRepository = studentRepository; } - public async Task> HandleAsync(GetStudents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetStudents query, CancellationToken cancellationToken) { - var students = await _studentRepository.FindAsync(_ => true); + var filter = Builders.Filter.Empty; + var options = new FindOptions + { + Limit = query.ResultsPerPage, + Skip = (query.Page - 1) * query.ResultsPerPage + }; - return students.Select(p => p.AsDto()); + using (var cursor = await _studentRepository.Collection.FindAsync(filter, options, cancellationToken)) + { + var documents = await cursor.ToListAsync(cancellationToken); + var dtos = documents.Select(s => s.AsDto()).ToList(); + var total = await _studentRepository.Collection.CountDocumentsAsync(filter); + return new Application.Queries.PagedResult(dtos, (int)total, query.ResultsPerPage, query.Page, BaseUrl); + } } - } -} + + } +} + diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/IExtendedStudentRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/IExtendedStudentRepository.cs new file mode 100644 index 000000000..24fa2e9e0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/IExtendedStudentRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public interface IExtendedStudentRepository : IStudentRepository + { + Task> FindAsync(FilterDefinition filter, int page, int pageSize, CancellationToken cancellationToken); + } +} 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 index 391ddfb51..f7b644982 100644 --- 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 @@ -1,11 +1,13 @@ using Convey.Persistence.MongoDB; +using MongoDB.Driver; +using MiniSpace.Services.Students.Application.Queries; 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 + public class StudentMongoRepository : IExtendedStudentRepository, IStudentRepository { private readonly IMongoRepository _repository; @@ -29,5 +31,30 @@ public Task UpdateAsync(Student student) public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + + public async Task> FindAsync(FilterDefinition filter, int page, int pageSize, CancellationToken cancellationToken) + { + var options = new FindOptions + { + Limit = pageSize, + Skip = (page - 1) * pageSize, + Sort = Builders.Sort.Descending(x => x.CreatedAt), + }; + + var result = await _repository.Collection + .FindAsync(filter, options, cancellationToken) + .ConfigureAwait(false); + + string baseUrl = "students"; + + // Fix CS4034: Mark the lambda expression as async + return new PagedResult(await result.ToListAsync(cancellationToken).ConfigureAwait(false), page, pageSize, (int)await CountAsync(filter, cancellationToken).ConfigureAwait(false), baseUrl); + } + + private async Task CountAsync(FilterDefinition filter, CancellationToken cancellationToken) + { + // Use the CountDocumentsAsync method of IMongoCollection to count documents that match the filter + return await _repository.Collection.CountDocumentsAsync(filter).ConfigureAwait(false); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/App.razor b/MiniSpace.Web/src/MiniSpace.Web/App.razor index 1c360b712..684b30e53 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/App.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/App.razor @@ -1,10 +1,47 @@ - - - - - - -

Sorry, there's nothing at this address.

-
-
-
+@* @using Microsoft.AspNetCore.Components.Authorization +@using Radzen +@using MiniSpace.Web.Areas.Students +@inherits LayoutComponentBase +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager +@inject Blazored.LocalStorage.ILocalStorageService localStorage +@inject CustomAuthenticationStateProvider CustomAuthenticationStateProvider + + + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
+
+ +@code { + protected override async Task OnInitializedAsync() + { + await CustomAuthenticationStateProvider.InitializeAsync(); + await base.OnInitializedAsync(); + } + +} *@ + + +@using Microsoft.AspNetCore.Components.Authorization + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
+
\ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs index 3b3dbde48..86d766afd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; using MiniSpace.Web.DTO; -using MiniSpace.Web.DTO.Data.Events; +using MiniSpace.Web.Data.Events; using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; @@ -28,16 +28,29 @@ public Task GetEventAsync(Guid eventId) public Task>> GetStudentEventsAsync(Guid studentId, int numberOfResults) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>>($"events/student/{studentId}"); + return _httpClient.GetAsync>>( + $"events/student/{studentId}?numberOfResults={numberOfResults}"); } - public Task AddEventAsync(Guid eventId, string name, Guid organizerId, DateTime startDate, DateTime endDate, + public Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + 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) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("events", new {eventId, name, organizerId, organizationId, + startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, description, + capacity, fee, category, publishDate}); + } + + public Task> UpdateEventAsync(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, DateTime publishDate) + string description, int capacity, decimal fee, string category, string publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("events", new {eventId, name, organizerId, startDate, endDate, buildingName, - street, buildingNumber, apartmentNumber, city, zipCode, description, capacity, fee, category, publishDate}); + return _httpClient.PutAsync($"events/{eventId}", new {eventId, name, organizerId, + startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, description, + capacity, fee, category, publishDate}); } public Task SignUpToEventAsync(Guid eventId, Guid studentId) @@ -46,22 +59,44 @@ public Task SignUpToEventAsync(Guid eventId, Guid studentId) return _httpClient.PostAsync($"events/{eventId}/sign-up", new {eventId, studentId}); } + public Task CancelSignUpToEventAsync(Guid eventId, Guid studentId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"events/{eventId}/sign-up?studentId={studentId}"); + } + public Task ShowInterestInEventAsync(Guid eventId, Guid studentId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync($"events/{eventId}/show-interest", new {eventId, studentId}); } + public Task CancelInterestInEventAsync(Guid eventId, Guid studentId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"events/{eventId}/show-interest?studentId={studentId}"); + } + public Task RateEventAsync(Guid eventId, int rating, Guid studentId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync($"events/{eventId}/rate", new {eventId, rating, studentId}); } - - public Task>> SearchEventsAsync(string name, string organizer, DateTime dateFrom, DateTime dateTo, PageableDto pageable) + + public Task>>> SearchEventsAsync(string name, + string organizer, string category, string state, IEnumerable friends, string friendsEngagementType, + string dateFrom, string dateTo, PageableDto pageable) { return _httpClient.PostAsync>>("events/search", - new (name, organizer, dateFrom, dateTo, pageable)); + new (name, organizer, category, state, friends, friendsEngagementType, dateFrom, dateTo, pageable)); + } + + public Task>>> SearchOrganizerEventsAsync(Guid organizerId, + string name, string state, string dateFrom, string dateTo, PageableDto pageable) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync>>("events/search/organizer", + new (name, organizerId, dateFrom, dateTo, state, pageable)); } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs index 6108fbe72..25cfc42f3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using MiniSpace.Web.DTO; using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Events { @@ -11,13 +12,23 @@ public interface IEventsService { Task GetEventAsync(Guid eventId); Task>> GetStudentEventsAsync(Guid studentId, int numberOfResults); - Task AddEventAsync(Guid eventId, string name, Guid organizerId, DateTime startDate, DateTime endDate, - string buildingName, string street, string buildingNumber, string apartmentNumber, string city, - string zipCode, string description, int capacity, decimal fee, string category, DateTime publishDate); + Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + 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); + Task> UpdateEventAsync(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); Task SignUpToEventAsync(Guid eventId, Guid studentId); + Task CancelSignUpToEventAsync(Guid eventId, Guid studentId); Task ShowInterestInEventAsync(Guid eventId, Guid studentId); + Task CancelInterestInEventAsync(Guid eventId, Guid studentId); Task RateEventAsync(Guid eventId, int rating, Guid studentId); - Task>> SearchEventsAsync(string name, string organizer, - DateTime dateFrom, DateTime dateTo, PageableDto pageable); + Task>>> SearchEventsAsync(string name, string organizer, + string category, string state, IEnumerable friends, string friendsEngagementType, string dateFrom, + string dateTo, PageableDto pageable); + Task>>> SearchOrganizerEventsAsync(Guid organizerId, + string name, string state, string dateFrom, string dateTo, PageableDto pageable); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs new file mode 100644 index 000000000..ca3e71a00 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Friends +{ + public class FriendsService : IFriendsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public FriendDto FriendDto { get; private set; } + + public FriendsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public async Task UpdateFriendDto(Guid friendId) + { + FriendDto = await GetFriendAsync(friendId); + } + + public void ClearFriendDto() + { + FriendDto = null; + } + + public async Task GetFriendAsync(Guid friendId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"friends/{friendId}"); + } + + public async Task> GetAllFriendsAsync(Guid studentId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + string url = $"friends/{studentId}"; + var friends = await _httpClient.GetAsync>(url); + + Console.WriteLine($"Retrieved {friends.Count()} friends for student ID {studentId}."); + + if (friends != null && friends.Any()) + { + foreach (var friend in friends) + { + friend.StudentDetails = await GetStudentAsync(friend.FriendId); + Console.WriteLine($"Friend ID: {friend.FriendId}, Friend's Student ID: {friend.StudentDetails.Id}, Name: {friend.StudentDetails.FirstName} {friend.StudentDetails.LastName}"); + } + } + else + { + Console.WriteLine("No friends found."); + } + + return friends; + } + + public async Task> AddFriendAsync(Guid friendId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.PostAsync("friends", new { friendId }); + } + + public async Task RemoveFriendAsync(Guid friendId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var requesterId = _identityService.GetCurrentUserId(); + Console.WriteLine($"Requester ID: {requesterId}"); // Log the requester ID + + if (requesterId == Guid.Empty) + { + Console.WriteLine("Invalid Requester ID: ID is empty."); + return; // Optionally handle the case where the requester ID is invalid + } + + var payload = new { RequesterId = requesterId, FriendId = friendId }; + Console.WriteLine($"Payload: {payload.RequesterId}, {payload.FriendId}"); + await _httpClient.DeleteAsync($"friends/{requesterId}/{friendId}/remove"); + } + + + + + public async Task> GetAllStudentsAsync() + { + if (_httpClient == null) throw new InvalidOperationException("HTTP client is not initialized."); + string accessToken = await _identityService.GetAccessTokenAsync(); + if (string.IsNullOrEmpty(accessToken)) + throw new InvalidOperationException("Invalid or missing access token."); + + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync>("students"); + } + + public async Task> GetAllStudentsAsync(int page = 1, int resultsPerPage = 10) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + string url = $"students?page={page}&resultsPerPage={resultsPerPage}"; + return await _httpClient.GetAsync>(url); + } + + public async Task GetStudentAsync(Guid studentId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{studentId}"); + } + + public async Task InviteStudent(Guid inviterId, Guid inviteeId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var payload = new { inviterId = inviterId, inviteeId = inviteeId }; + await _httpClient.PostAsync>($"friends/{inviteeId}/invite", payload); + } + + // public async Task> GetSentFriendRequestsAsync() + // { + // var studentId = _identityService.GetCurrentUserId(); + // string accessToken = await _identityService.GetAccessTokenAsync(); + // _httpClient.SetAccessToken(accessToken); + // return await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); + // } + + public async Task GetUserDetails(Guid userId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{userId}"); + } + + + public async Task> GetSentFriendRequestsAsync() + { + try + { + var studentId = _identityService.GetCurrentUserId(); + if (studentId == Guid.Empty) + { + throw new InvalidOperationException("User ID is not valid."); + } + + string accessToken = await _identityService.GetAccessTokenAsync(); + if (string.IsNullOrEmpty(accessToken)) + { + throw new InvalidOperationException("Access token is missing or invalid."); + } + + _httpClient.SetAccessToken(accessToken); + var friendRequests = await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); + + foreach (var request in friendRequests) + { + var userDetails = await GetUserDetails(request.InviteeId); + request.InviteeName = userDetails.FirstName + " " + userDetails.LastName; + request.InviteeEmail = userDetails.Email; + request.InviteeImage = userDetails.ProfileImage; + } + + return friendRequests; + } + catch (Exception ex) + { + return new List(); + } + } + + public async Task> GetIncomingFriendRequestsAsync() + { + var userId = _identityService.GetCurrentUserId(); + if (userId == Guid.Empty) + { + throw new InvalidOperationException("User ID is not valid."); + } + + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var endpoint = $"friends/requests/{userId}"; + return await _httpClient.GetAsync>(endpoint); + } + + public async Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var payload = new { RequesterId = requesterId, FriendId = friendId }; + await _httpClient.PostAsync($"friends/requests/{requestId}/accept", payload); + } + + public async Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + var payload = new { RequesterId = requesterId, FriendId = friendId }; + await _httpClient.PostAsync($"friends/requests/{requestId}/decline", payload); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs new file mode 100644 index 000000000..7858e686e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Friends +{ + public interface IFriendsService + { + FriendDto FriendDto { get; } + Task UpdateFriendDto(Guid friendId); + void ClearFriendDto(); + Task GetFriendAsync(Guid friendId); + Task> AddFriendAsync(Guid friendId); + Task RemoveFriendAsync(Guid friendId); + Task> GetAllFriendsAsync(Guid studentId); + Task GetStudentAsync(Guid studentId); + Task> GetAllStudentsAsync(); + Task> GetAllStudentsAsync(int page = 1, int resultsPerPage = 10); + Task InviteStudent(Guid inviterId, Guid inviteeId); + Task> GetSentFriendRequestsAsync(); + Task> GetIncomingFriendRequestsAsync(); + Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); + Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs new file mode 100644 index 000000000..3bdd08e0b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs @@ -0,0 +1,12 @@ + +using System.Collections; +using System.Collections.Generic; + +namespace MiniSpace.Web.Areas.Friends +{ + public class PagedResult + { + public IEnumerable Data { get; set; } + public int TotalCount { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/ErrorMapperService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/ErrorMapperService.cs new file mode 100644 index 000000000..2709fcd7a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/ErrorMapperService.cs @@ -0,0 +1,24 @@ +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Http +{ + public class ErrorMapperService : IErrorMapperService + { + public string MapError(ErrorMessage error) + { + switch (error.Code) + { + case "invalid_credentials": + return "Failed to sign in. Please check your credentials and try again."; + case "email_in_use": + return $"{error.Reason} Please try again with a different email."; + case "invalid_event_date_time": + return "Please review the selected dates. They should be in chronological order."; + case "invalid_student_date_of_birth": + return "Please review the student's date of birth. It should be in the past."; + default: + return "Something went wrong. Please try again later."; + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/IErrorMapperService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/IErrorMapperService.cs new file mode 100644 index 000000000..cfdbff5df --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Http/IErrorMapperService.cs @@ -0,0 +1,10 @@ +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Http +{ + public interface IErrorMapperService + { + string MapError(ErrorMessage error); + } +} + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/CustomAuthenticationStateProvider.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/CustomAuthenticationStateProvider.cs new file mode 100644 index 000000000..6077f8893 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/CustomAuthenticationStateProvider.cs @@ -0,0 +1,80 @@ +using Blazored.LocalStorage; +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using System.IdentityModel.Tokens.Jwt; +using System; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using System.Text.Json; +using Microsoft.IdentityModel.JsonWebTokens; +using MiniSpace.Web.Areas.Identity; +using System.Collections.Generic; + +public class CustomAuthenticationStateProvider : AuthenticationStateProvider +{ + private readonly ILocalStorageService _localStorage; + private readonly JwtSecurityTokenHandler _jwtHandler = new JwtSecurityTokenHandler(); + private readonly IIdentityService _identityService; + + public CustomAuthenticationStateProvider(ILocalStorageService localStorage, IIdentityService identityService) + { + _localStorage = localStorage; + _identityService = identityService; + _identityService.InitializeAuthenticationState().ConfigureAwait(false); + } + + public override async Task GetAuthenticationStateAsync() + { + string storedToken = await _localStorage.GetItemAsStringAsync("jwtDto"); + if (!string.IsNullOrEmpty(storedToken)) + { + var jwtDto = JsonSerializer.Deserialize(storedToken); + if (jwtDto != null && ValidateToken(jwtDto.AccessToken)) + { + var claims = ParseClaimsFromJwt(jwtDto.AccessToken); + var identity = new ClaimsIdentity(claims, "jwtAuthType"); + var user = new ClaimsPrincipal(identity); + return new AuthenticationState(user); + } + } + + return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); + } + + private IEnumerable ParseClaimsFromJwt(string jwt) + { + var token = _jwtHandler.ReadJwtToken(jwt); + return token.Claims; + } + + private bool ValidateToken(string jwt) + { + if (string.IsNullOrEmpty(jwt)) return false; + var token = _jwtHandler.ReadJwtToken(jwt); + return token.ValidTo > DateTime.UtcNow; + } + + // public async Task InitializeAsync() + // { + // string storedToken = await _localStorage.GetItemAsStringAsync("jwtDto"); + // if (!string.IsNullOrEmpty(storedToken)) + // { + // var jwtDto = JsonSerializer.Deserialize(storedToken); + // if (jwtDto != null && ValidateToken(jwtDto.AccessToken)) + // { + // var claims = ParseClaimsFromJwt(jwtDto.AccessToken); + // var identity = new ClaimsIdentity(claims, "jwtAuthType"); + // var user = new ClaimsPrincipal(identity); + // NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); + // } + // } + // } + +public async Task InitializeAsync() + { + var authState = await GetAuthenticationStateAsync(); + NotifyAuthenticationStateChanged(Task.FromResult(authState)); + } + + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs index 008afbeb6..47895ac41 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs @@ -1,16 +1,30 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Identity { public interface IIdentityService { - 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(); + public JwtDto JwtDto { get; set;} + public UserDto UserDto { get; set; } + bool IsAuthenticated { get; set; } + Task GetAccountAsync(JwtDto jwtDto); + Task> SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", IEnumerable permissions = null); + Task> SignInAsync(string email, string password); + Task Logout(); + Task GetAccessTokenAsync(); + Task InitializeAuthenticationState(); + Task CheckIfUserIsAuthenticated(); + Task IsTokenValid(); + public Guid GetCurrentUserId(); + public string GetCurrentUserRole(); + + Task GrantOrganizerRights(Guid userId); + Task RevokeOrganizerRights(Guid userId); + Task BanUser(Guid userId); + Task UnbanUser(Guid userId); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/Identity.razor.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/Identity.razor.cshtml index 33221eaa5..481b7f1f7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/Identity.razor.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/Identity.razor.cshtml @@ -14,9 +14,9 @@ @functions { - protected async Task OnInitializedAsync() + @* protected async Task OnInitializedAsync() { await IdentityComponent.OnInit(); - } + } *@ } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs index cfdb7802c..6e4ee255f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Identity { @@ -20,10 +21,10 @@ public Task OnInit() 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) + public Task> SignInAsync(string email, string password) => _identityService.SignInAsync(email, password); - public Task GetAccount() - => _identityService.GetAccountAsync(); + public Task GetAccount(JwtDto jwtDto) + => _identityService.GetAccountAsync(jwtDto); } } \ 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 0396c3e11..30892d2f7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -1,5 +1,11 @@ +using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Blazored.LocalStorage; +using Microsoft.AspNetCore.Components; using MiniSpace.Web.DTO; using MiniSpace.Web.HttpClients; @@ -8,34 +14,284 @@ namespace MiniSpace.Web.Areas.Identity class IdentityService : IIdentityService { private readonly IHttpClient _httpClient; - public JwtDto JwtDto { get; private set; } - public bool IsAuthenticated { get; private set; } + private readonly JwtSecurityTokenHandler _jwtHandler; + private readonly ILocalStorageService _localStorage; + private readonly NavigationManager _navigationManager; - public IdentityService(IHttpClient httpClient) + public JwtDto JwtDto { get; set; } + public UserDto UserDto { get; set; } + public string Name {get; private set; } + public string Email {get; private set; } + public bool IsAuthenticated { get; set; } + + public IdentityService(IHttpClient httpClient, ILocalStorageService localStorage, NavigationManager navigationManager) { _httpClient = httpClient; + _jwtHandler = new JwtSecurityTokenHandler(); + _localStorage = localStorage; + _navigationManager = navigationManager; } - public Task GetAccountAsync() + public Task GetAccountAsync(JwtDto jwtDto) { - _httpClient.SetAccessToken(JwtDto.AccessToken); + _httpClient.SetAccessToken(jwtDto.AccessToken); return _httpClient.GetAsync("identity/me"); } - 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 async Task> SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", + IEnumerable permissions = null) + { + return await _httpClient.PostAsync("identity/sign-up", + new {firstName, lastName, email, password, role, permissions}); + + } + + // public async Task> SignInAsync(string email, string password) + // { + // var response = await _httpClient.PostAsync("identity/sign-in", new {email, password}); + // JwtDto = response.Content; + + // if (JwtDto != null) + // { + // var jwtToken = _jwtHandler.ReadJwtToken(JwtDto.AccessToken); + // var payload = jwtToken.Payload; + // UserDto = await GetAccountAsync(JwtDto); + // Name = (string)payload["name"]; + // Email = (string)payload["e-mail"]; + // IsAuthenticated = true; + // } + + // return response; + // } - public async Task SignInAsync(string email, string password) + public async Task> SignInAsync(string email, string password) { - JwtDto = await _httpClient.PostAsync("identity/sign-in", new {email, password}); - IsAuthenticated = true; - return JwtDto; + var response = await _httpClient.PostAsync("identity/sign-in", new { email, password }); + if (response.Content != null) + { + JwtDto = response.Content; + var jwtDtoJson = JsonSerializer.Serialize(JwtDto); + await _localStorage.SetItemAsStringAsync("jwtDto", jwtDtoJson); + + var jwtToken = _jwtHandler.ReadJwtToken(JwtDto.AccessToken); + var payload = jwtToken.Payload; + UserDto = await GetAccountAsync(JwtDto); + Name = payload.Claims.FirstOrDefault(c => c.Type == "name")?.Value; + Email = payload.Claims.FirstOrDefault(c => c.Type == "e-mail")?.Value; + IsAuthenticated = true; + } + return response; } - public void Logout() + public async Task Logout() { + await _localStorage.RemoveItemAsync("jwtDto"); JwtDto = null; + UserDto = null; + Name = null; + Email = null; IsAuthenticated = false; + _navigationManager.NavigateTo("signin", forceLoad: true); + } + + private async Task RefreshAccessToken(string refreshToken) + { + var payload = new { refreshToken }; + var response = await _httpClient.PostAsync("identity/refresh-token", payload); + if (response.ErrorMessage != null) + { + throw new InvalidOperationException($"Error refreshing token: {response.ErrorMessage.Reason}"); + } + + if (response.Content != null) + { + return response.Content; + } + + throw new InvalidOperationException("Failed to refresh token"); + } + + // Make the Logout asynchronous 😕 + // public void Logout() + // { + // JwtDto = null; + // UserDto = null; + // Name = null; + // Email = null; + // IsAuthenticated = false; + // } + public async Task GetAccessTokenAsync() + { + var jwtDtoJson = await _localStorage.GetItemAsStringAsync("jwtDto"); + + if (!string.IsNullOrEmpty(jwtDtoJson)) + { + JwtDto jwtDto = JsonSerializer.Deserialize(jwtDtoJson); + + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + + if (jwtToken.ValidTo > DateTime.UtcNow) + { + return jwtDto.AccessToken; + } + else + { + if (!string.IsNullOrEmpty(jwtDto.RefreshToken)) + { + try + { + JwtDto newJwtDto = await RefreshAccessToken(jwtDto.RefreshToken); + + if (newJwtDto != null) + { + var newJwtDtoJson = JsonSerializer.Serialize(newJwtDto); + + await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); + + return newJwtDto.AccessToken; + } + } + catch (Exception ex) + { + await _localStorage.RemoveItemAsync("jwtDto"); + + _navigationManager.NavigateTo("signin", forceLoad: true); + + throw new InvalidOperationException("Failed to refresh token: " + ex.Message); + } + } + + await _localStorage.RemoveItemAsync("jwtDto"); + + _navigationManager.NavigateTo("signin", forceLoad: true); + + throw new InvalidOperationException("Session expired, please login again."); + } + } + + throw new InvalidOperationException("Authentication required."); + } + + public async Task InitializeAuthenticationState() + { + var jwtDtoJson = await _localStorage.GetItemAsStringAsync("jwtDto"); + if (!string.IsNullOrEmpty(jwtDtoJson)) + { + JwtDto = JsonSerializer.Deserialize(jwtDtoJson); + var tokenExpirationDateTime = DateTimeOffset.FromUnixTimeSeconds(JwtDto.Expires).UtcDateTime; + if (JwtDto != null && DateTime.UtcNow < tokenExpirationDateTime) + { + UserDto = await GetAccountAsync(JwtDto); + IsAuthenticated = UserDto != null; + } + else if (JwtDto != null && !string.IsNullOrEmpty(JwtDto.RefreshToken)) + { + JwtDto = await RefreshAccessToken(JwtDto.RefreshToken); + if (JwtDto != null) + { + jwtDtoJson = JsonSerializer.Serialize(JwtDto); + await _localStorage.SetItemAsStringAsync("jwtDto", jwtDtoJson); + UserDto = await GetAccountAsync(JwtDto); + IsAuthenticated = UserDto != null; + } + } + } + } + + public async Task CheckIfUserIsAuthenticated() + { + var jwtDtoJson = await _localStorage.GetItemAsStringAsync("jwtDto"); + if (!string.IsNullOrEmpty(jwtDtoJson)) + { + JwtDto jwtDto = JsonSerializer.Deserialize(jwtDtoJson); + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + if (jwtToken.ValidTo > DateTime.UtcNow) + { + IsAuthenticated = true; + } + else + { + IsAuthenticated = await TryRefreshToken(jwtDto.RefreshToken); + } + } + else + { + IsAuthenticated = false; + } + return IsAuthenticated; + } + + private async Task TryRefreshToken(string refreshToken) + { + try + { + var response = await _httpClient.PostAsync("identity/refresh-token", new { refreshToken }); + if (response.Content != null) + { + var newJwtDtoJson = JsonSerializer.Serialize(response.Content); + await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); + return true; + } + } + catch (Exception) + { + await Logout(); + } + return false; + } + + public async Task IsTokenValid() + { + var jwtDtoJson = await _localStorage.GetItemAsStringAsync("jwtDto"); + if (!string.IsNullOrEmpty(jwtDtoJson)) + { + var jwtDto = JsonSerializer.Deserialize(jwtDtoJson); + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + return jwtToken.ValidTo > DateTime.UtcNow; + } + return false; + } + + public Guid GetCurrentUserId() + { + if (UserDto != null && UserDto.Id != Guid.Empty) + { + return UserDto.Id; + } + throw new InvalidOperationException("No user is currently logged in."); + } + + public string GetCurrentUserRole() + { + if (UserDto != null && UserDto.Id != Guid.Empty) + { + return UserDto.Role; + } + return string.Empty; + } + + public Task GrantOrganizerRights(Guid userId) + { + _httpClient.SetAccessToken(JwtDto.AccessToken); + return _httpClient.PostAsync($"identity/users/{userId}/organizer-rights", new {userId}); + } + + public Task RevokeOrganizerRights(Guid userId) + { + _httpClient.SetAccessToken(JwtDto.AccessToken); + return _httpClient.DeleteAsync($"identity/users/{userId}/organizer-rights"); + } + + public Task BanUser(Guid userId) + { + _httpClient.SetAccessToken(JwtDto.AccessToken); + return _httpClient.PostAsync($"identity/users/{userId}/ban", new {userId}); + } + + public Task UnbanUser(Guid userId) + { + _httpClient.SetAccessToken(JwtDto.AccessToken); + return _httpClient.DeleteAsync($"identity/users/{userId}/ban"); } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs new file mode 100644 index 000000000..cefb8d315 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Organizations +{ + public interface IOrganizationsService + { + Task GetOrganizationAsync(Guid organizationId); + Task GetOrganizationDetailsAsync(Guid organizationId); + Task> GetOrganizerOrganizationsAsync(Guid organizerId); + Task> GetRootOrganizationsAsync(); + Task> GetChildrenOrganizationsAsync(Guid organizationId); + Task> AddOrganization(Guid organizationId, string name, Guid parentId); + Task AddOrganizerToOrganization(Guid organizationId, Guid organizerId); + Task RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs new file mode 100644 index 000000000..f447a01fb --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Organizations +{ + public class OrganizationsService : IOrganizationsService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public OrganizationsService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task GetOrganizationAsync(Guid organizationId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync($"organizations/{organizationId}"); + } + + public Task GetOrganizationDetailsAsync(Guid organizationId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync($"organizations/{organizationId}/details"); + } + + public Task> GetOrganizerOrganizationsAsync(Guid organizerId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync>($"organizations/organizer/{organizerId}"); + } + + public Task> GetRootOrganizationsAsync() + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync>("organizations/root"); + } + + public Task> GetChildrenOrganizationsAsync(Guid organizationId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.GetAsync> + ($"organizations/{organizationId}/children?parentId={organizationId}"); + } + + public Task> AddOrganization(Guid organizationId, string name, Guid parentId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("organizations", new {organizationId, name, parentId}); + } + + public Task AddOrganizerToOrganization(Guid organizationId, Guid organizerId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/organizer", + new {organizationId, organizerId}); + } + + public Task RemoveOrganizerFromOrganization(Guid organizationId, Guid organizerId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"organizations/{organizationId}/organizer/{organizerId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs index 2cf058fec..81ce84a83 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs @@ -2,16 +2,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Posts { public interface IPostsService { + Task GetPostAsync(Guid postId); Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate); - Task CreatePostAsync(Guid postId, Guid eventId, Guid studentId, string textContext, string mediaContext, - string state, DateTime publishedDate); + Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContext, + string mediaContext, string state, DateTime? publishDate); Task DeletePostAsync(Guid postId); Task> GetPostsAsync(Guid eventId); - Task UpdatePostAsync(Guid postId, string textContext, string mediaContext); + Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs index cf70b38f0..541ad86ba 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs @@ -17,6 +17,11 @@ public PostsService(IHttpClient httpClient, IIdentityService identityService) _httpClient = httpClient; _identityService = identityService; } + + public Task GetPostAsync(Guid postId) + { + return _httpClient.GetAsync($"posts/{postId}"); + } public Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate) { @@ -24,11 +29,12 @@ public Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate return _httpClient.PutAsync($"posts/{postId}/state/{state}", new {postId, state, publishDate}); } - public Task CreatePostAsync(Guid postId, Guid eventId, Guid studentId, string textContext, string mediaContext, string state, - DateTime publishedDate) + public Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContent, + string mediaContext, string state, DateTime? publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("posts", new {postId, eventId, studentId, textContext, mediaContext, state, publishedDate}); + return _httpClient.PostAsync("posts", new {postId, eventId, organizerId, textContent, + mediaContext, state, publishDate}); } public Task DeletePostAsync(Guid postId) @@ -39,13 +45,13 @@ public Task DeletePostAsync(Guid postId) public Task> GetPostsAsync(Guid eventId) { - return _httpClient.GetAsync>("posts"); + return _httpClient.GetAsync>($"posts?eventId={eventId}"); } - public Task UpdatePostAsync(Guid postId, string textContext, string mediaContext) + public Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PutAsync($"posts/{postId}", new {postId, textContext, mediaContext}); + return _httpClient.PutAsync($"posts/{postId}", new {postId, textContent, mediaContent}); } } } \ 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 index 3932d86a5..d6a8e8c0c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -2,14 +2,20 @@ using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students { public interface IStudentsService { + StudentDto StudentDto { get; } + Task UpdateStudentDto(Guid studentId); + void ClearStudentDto(); Task GetStudentAsync(Guid studentId); + Task> GetStudentsAsync(); Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications); - Task CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, string description, DateTime dateOfBirth, bool emailNotifications); + Task GetStudentStateAsync(Guid studentId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index 427f5fd5a..2d1afedaa 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; using MiniSpace.Web.DTO; @@ -11,16 +12,37 @@ public class StudentsService : IStudentsService private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + public StudentDto StudentDto { get; private set; } + public StudentsService(IHttpClient httpClient, IIdentityService identityService) { _httpClient = httpClient; _identityService = identityService; } + + public async Task UpdateStudentDto(Guid studentId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + StudentDto = await _httpClient.GetAsync($"students/{studentId}"); + } + + public void ClearStudentDto() + { + StudentDto = null; + } - public Task GetStudentAsync(Guid studentId) + public async Task GetStudentAsync(Guid studentId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{studentId}"); + } + + public Task> GetStudentsAsync() { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync($"students/{studentId}"); + return _httpClient.GetAsync>("students"); } public Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications) @@ -30,9 +52,15 @@ public Task UpdateStudentAsync(Guid studentId, string profileImage, string descr description, emailNotifications}); } - public Task CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + public Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, string description, DateTime dateOfBirth, bool emailNotifications) - => _httpClient.PostAsync("students", new {studentId, profileImage, + => _httpClient.PostAsync("students", new {studentId, profileImage, description, dateOfBirth, emailNotifications}); + + public async Task GetStudentStateAsync(Guid studentId) + { + var student = await GetStudentAsync(studentId); + return student != null ? student.State : "invalid"; + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs index ce54cc5a4..4e4f40d1f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs @@ -11,7 +11,6 @@ public class EventDto 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 int InterestedStudents { get; set; } public int SignedUpStudents { get; set; } @@ -20,5 +19,11 @@ public class EventDto public string Category { get; set; } public string Status { get; set; } public DateTime PublishDate { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsSignedUp { get; set; } + public bool IsInterested { get; set; } + public bool HasRated { get; set; } + public IEnumerable FriendsInterestedIn { get; set; } + public IEnumerable FriendsSignedUp { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs new file mode 100644 index 000000000..e00cb739a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.States; + +namespace MiniSpace.Web.DTO +{ + public class FriendDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public Guid StudentId { get; set; } + public Guid FriendId { get; set; } + public string LastName { get; set; } + public string FullName => $"{FirstName} {LastName}"; + public DateTime CreatedAt { get; set; } + public FriendState State { get; set; } + public string ProfileImage { get; set; } + public StudentDto StudentDetails { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs new file mode 100644 index 000000000..a39262bc9 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs @@ -0,0 +1,19 @@ +using System; +using MiniSpace.Web.DTO.States; + +namespace MiniSpace.Services.Friends.Application.Dto +{ + public class FriendRequestDto + { + public Guid Id { get; set; } + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + public DateTime RequestedAt { get; set; } + public FriendState State { get; set; } + public Guid StudentId { get; set; } + + public string InviteeName { get; set; } + public string InviteeEmail { get; set; } + public string InviteeImage { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs new file mode 100644 index 000000000..dfaa9bd93 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO +{ + public class OrganizationDetailsDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + public IEnumerable Organizers { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs new file mode 100644 index 000000000..91138784b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class OrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizerDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizerDto.cs index 6c973fc83..38f8ca11e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizerDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizerDto.cs @@ -7,6 +7,7 @@ public class OrganizerDto public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } - public string Organization { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/PaginatedResponseDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/PaginatedResponseDto.cs new file mode 100644 index 000000000..9f75b140f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/PaginatedResponseDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO +{ + public class PaginatedResponseDto + { + public List Results { get; set; } + public int Total { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public string NextPage { get; set; } + public string PrevPage { get; set; } + } + +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ParticipantDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ParticipantDto.cs new file mode 100644 index 000000000..b684fa121 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ParticipantDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class ParticipantDto + { + public Guid StudentId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs index 2b9cc7596..966461718 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs @@ -6,11 +6,12 @@ public class PostDto { public Guid Id { get; set; } public Guid EventId { get; set; } - public Guid StudentId { get; set; } + public Guid OrganizerId { 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; } + public DateTime? UpdatedAt { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs new file mode 100644 index 000000000..5ccd31ad1 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/States/FriendState.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Web.DTO.States +{ + public enum FriendState + { + Unknown, + Requested, + Accepted, + Declined, + Blocked, + Cancelled, + Confirmed + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs index b26aaeac1..0400d6cd8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs @@ -14,9 +14,13 @@ public class StudentDto public string Description { get; set; } public DateTime DateOfBirth { get; set; } public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public bool IsOrganizer { get; set; } public string State { get; set; } public DateTime CreatedAt { get; set; } public IEnumerable InterestedInEvents { get; set; } public IEnumerable SignedUpEvents { get; set; } + public bool IsInvitationPending { get; set; } + public bool InvitationSent { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs index dc39d7ef8..6d8c5248f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/UserDto.cs @@ -3,7 +3,7 @@ namespace MiniSpace.Web.DTO { public class UserDto - { + { public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs index b8e8c7780..ad9d1916e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchEvents.cs @@ -1,20 +1,30 @@ using System; +using System.Collections.Generic; using MiniSpace.Web.DTO.Wrappers; -namespace MiniSpace.Web.DTO.Data.Events +namespace MiniSpace.Web.Data.Events { public class SearchEvents { public string Name { get; set; } public string Organizer { get; set; } - public DateTime DateFrom { get; set; } - public DateTime DateTo { get; set; } + public string Category { get; set; } + public string State { get; set; } + public IEnumerable Friends { get; set; } + public string FriendsEngagementType { get; set; } + public string DateFrom { get; set; } + public string DateTo { get; set; } public PageableDto Pageable { get; set; } - public SearchEvents(string name, string organizer, DateTime dateFrom, DateTime dateTo, PageableDto pageable) + public SearchEvents(string name, string organizer, string category, string state, IEnumerable friends, + string friendsEngagementType, string dateFrom, string dateTo, PageableDto pageable) { Name = name; Organizer = organizer; + Category = category; + State = state; + Friends = friends; + FriendsEngagementType = friendsEngagementType; DateFrom = dateFrom; DateTo = dateTo; Pageable = pageable; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchOrganizerEvents.cs b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchOrganizerEvents.cs new file mode 100644 index 000000000..5a85e4a82 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Data/Events/SearchOrganizerEvents.cs @@ -0,0 +1,26 @@ +using System; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Data.Events +{ + public class SearchOrganizerEvents + { + public string Name { get; set; } + public Guid OrganizerId { get; set; } + public string DateFrom { get; set; } + public string DateTo { get; set; } + public string State { get; set; } + public PageableDto Pageable { get; set; } + + public SearchOrganizerEvents(string name, Guid organizerId, string dateFrom, string dateTo, + string state, PageableDto pageable) + { + Name = name; + OrganizerId = organizerId; + DateFrom = dateFrom; + DateTo = dateTo; + State = state; + Pageable = pageable; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs index 74612dd59..90d065e5f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Polly; +using MiniSpace.Web.Areas.Friends; namespace MiniSpace.Web.HttpClients { @@ -49,35 +50,58 @@ public Task PostAsync(string uri, T request) return TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); } - public async Task PostAsync(string uri, TRequest request) + public async Task> PostAsync(string uri, TRequest request) { var jsonPayload = JsonConvert.SerializeObject(request, JsonSerializerSettings); _logger.LogDebug($"Sending HTTP POST request to URI: {uri} with payload: {jsonPayload}"); var (success, content) = await TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); - return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return !success ? new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)) + : new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)); } public Task PutAsync(string uri, T request) => TryExecuteAsync(uri, client => client.PutAsync(uri, GetPayload(request))); - public async Task PutAsync(string uri, TRequest request) + public async Task> PutAsync(string uri, TRequest request) { var (success, content) = await TryExecuteAsync(uri, client => client.PutAsync(uri, GetPayload(request))); - return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return !success ? new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)) + : new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)); } public Task DeleteAsync(string uri) => TryExecuteAsync(uri, client => client.DeleteAsync(uri)); + public async Task DeleteAsync(string uri, object payload) + { + var jsonPayload = JsonConvert.SerializeObject(payload, JsonSerializerSettings); + _logger.LogDebug($"Sending HTTP DELETE request to URI: {uri} with payload: {jsonPayload}"); + + var request = new HttpRequestMessage(HttpMethod.Delete, uri) + { + Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + }; + + var response = await _client.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Error response from server: {errorContent}"); + throw new HttpRequestException($"Request to {uri} failed with status code {response.StatusCode} and message {errorContent}"); + } + } + + + private static StringContent GetPayload(T request) -{ - var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); - // Set content type with charset parameter - return new StringContent(json, Encoding.UTF8, "text/plain"); -} + { + var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); + // Set content type with charset parameter + return new StringContent(json, Encoding.UTF8, "text/plain"); + } @@ -109,18 +133,23 @@ private static StringContent GetPayload(T request) return (true, await response.Content.ReadAsStringAsync()); } + var errorContent = "invalid_http_response"; if (!response.IsSuccessStatusCode) { - var errorContent = await response.Content.ReadAsStringAsync(); + errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError($"Error response from server: {errorContent}"); } _logger.LogError($"Received an invalid response to HTTP request from URI: {uri}" + $"{Environment.NewLine}{response}"); - return default; + return (false, errorContent); } }); + + + + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/ErrorMessage.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/ErrorMessage.cs new file mode 100644 index 000000000..461aea9c6 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/ErrorMessage.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Web.HttpClients +{ + public class ErrorMessage + { + public string Code { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs new file mode 100644 index 000000000..b6121759b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Web.HttpClients +{ + public class HttpResponse + { + public T Content { get; set; } + public ErrorMessage ErrorMessage { get; set; } + + public HttpResponse(T content) + { + Content = content; + } + + public HttpResponse(ErrorMessage errorMessage) + { + ErrorMessage = errorMessage; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/IHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/IHttpClient.cs index cd9b95b97..590c742e1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/IHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/IHttpClient.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using MiniSpace.Web.Areas.Friends; namespace MiniSpace.Web.HttpClients { @@ -7,9 +8,10 @@ public interface IHttpClient void SetAccessToken(string accessToken); Task GetAsync(string uri); Task PostAsync(string uri, T request); - Task PostAsync(string uri, TRequest request); + Task> PostAsync(string uri, TRequest request); Task PutAsync(string uri, T request); - Task PutAsync(string uri, TRequest request); + Task> PutAsync(string uri, TRequest request); Task DeleteAsync(string uri); + Task DeleteAsync(string uri, object payload); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index 1636d5826..1df37b7db 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -6,6 +6,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs new file mode 100644 index 000000000..8e79c5b13 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/AddEventModel.cs @@ -0,0 +1,25 @@ +using System; + +namespace MiniSpace.Web.Models.Events +{ + public class AddEventModel + { + public Guid EventId { get; set; } + public string Name { get; set; } + public Guid OrganizerId { get; set; } + public Guid OrganizationId { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + 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 string Description { get; set; } + public int Capacity { get; set; } + public decimal Fee { get; set; } + public string Category { get; set; } + public DateTime PublishDate { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs new file mode 100644 index 000000000..a4e187217 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchEventsModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Models.Events +{ + public class SearchEventsModel + { + public string Name { get; set; } + public string Organizer { get; set; } + public string Category { get; set; } + public string State { get; set; } + public IEnumerable Friends { get; set; } + public string FriendsEngagementType { get; set; } + public DateTime DateFrom { get; set; } + public DateTime DateTo { get; set; } + public PageableDto Pageable { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchOrganizerEventsModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchOrganizerEventsModel.cs new file mode 100644 index 000000000..5404c11ec --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/SearchOrganizerEventsModel.cs @@ -0,0 +1,15 @@ +using System; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Models.Events +{ + public class SearchOrganizerEventsModel + { + public Guid OrganizerId { get; set; } + public string Name { get; set; } + public string State { get; set; } + public DateTime DateFrom { get; set; } + public DateTime DateTo { get; set; } + public PageableDto Pageable { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs new file mode 100644 index 000000000..f25a8a737 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs @@ -0,0 +1,24 @@ +using System; + +namespace MiniSpace.Web.Models.Events +{ + public class UpdateEventModel + { + public Guid EventId { get; set; } + public string Name { get; set; } + public Guid OrganizerId { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + 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 string Description { get; set; } + public int Capacity { get; set; } + public decimal Fee { get; set; } + public string Category { get; set; } + public DateTime PublishDate { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs new file mode 100644 index 000000000..d2316ace8 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Web.Models.Posts +{ + public class CreatePostModel + { + public Guid PostId { get; set; } + public Guid EventId { get; set; } + public Guid OrganizerId { get; set; } + public string TextContent { get; set; } + public string MediaContent { get; set; } + public string State { get; set; } + public DateTime PublishDate { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs new file mode 100644 index 000000000..679923924 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace MiniSpace.Web.Models.Posts +{ + public class UpdatePostModel + { + public Guid PostId { get; set; } + public string TextContent { get; set; } + public string MediaContent { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor similarity index 87% rename from MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor rename to MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor index a7db6723f..a0803adf5 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/CompleteRegistration.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor @@ -4,16 +4,19 @@ @using MiniSpace.Web.Components @using MiniSpace.Web.Models.Students @using System.IO +@using MiniSpace.Web.Areas.Http @using Radzen @inject IIdentityService IdentityService @inject IStudentsService StudentsService +@inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager

Complete your registration

+

This step is necessary to go ahead, otherwise please to sign out.

- Failed to load image. + @errorMessage + Style="width: 20em" class="rz-border-radius-10"/> - + } else @@ -93,8 +93,8 @@ else { if (IdentityService.IsAuthenticated) { - var userDto = await IdentityService.GetAccountAsync(); - studentDto = await StudentsService.GetStudentAsync(userDto.Id); + await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); + studentDto = StudentsService.StudentDto; } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor similarity index 62% rename from MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor rename to MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor index 803f2b686..7c4c32354 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor @@ -3,20 +3,21 @@ @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Students @using Radzen +@using MiniSpace.Web.Areas.Http @inject IIdentityService IdentityService @inject IStudentsService StudentsService +@inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager

Welcome to MiniSpace

Please sign in to access your account and manage your details.

+
 
+

Sign In

- -

Sign In

- -
+
- Failed to sign in. Please check your credentials and try again. + @errorMessage @@ -48,7 +49,7 @@ - +
@@ -57,26 +58,57 @@ +
@code { private SignInModel signInModel = new SignInModel(); private bool showError = false; + private string errorMessage = string.Empty; private bool popup; - private async Task HandleSignIn() + @* private async Task HandleSignIn() { - var jwtDto = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); + var response = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); + var jwtDto = response.Content; if (jwtDto != null && !string.IsNullOrEmpty(jwtDto.AccessToken)) { - var userDto = await IdentityService.GetAccountAsync(); - var studentDto = await StudentsService.GetStudentAsync(userDto.Id); + await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); + NavigationManager.NavigateTo(StudentsService.StudentDto.State == "incomplete" ? "/signup/complete" : "/account"); + } + else + { + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); + StateHasChanged(); // Force the component to re-render + } + } *@ - NavigationManager.NavigateTo(studentDto.State == "incomplete" ? "/signup/complete" : "/account"); + private async Task HandleSignIn() +{ + try + { + var response = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); + if (response != null && response.Content != null && !string.IsNullOrEmpty(response.Content.AccessToken)) + { + // Assuming UpdateStudentDto and other subsequent methods correctly handle null checks + await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); + var nextPage = StudentsService.StudentDto.State == "incomplete" ? "/signup/complete" : "/account"; + NavigationManager.NavigateTo(nextPage); } else { showError = true; + errorMessage = "Invalid login attempt"; StateHasChanged(); // Force the component to re-render } } + catch (Exception ex) + { + // Log the exception or handle it accordingly + showError = true; + errorMessage = $"Error during sign in: {ex.Message}"; + StateHasChanged(); // Update UI to show error message + } +} + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor similarity index 66% rename from MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor rename to MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor index 9619bc8f4..200a6e8b2 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignUp.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor @@ -1,67 +1,74 @@ @page "/signup" @using MiniSpace.Web.Areas.Identity @using Radzen +@using MiniSpace.Web.Areas.Http @inject IIdentityService IdentityService +@inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager

Welcome to MiniSpace

-

Please sign in to access your account and manage your details.

- +

Please sign up to create your account and explore our world to the full.

+
 

Sign Up

-
+
+ + @errorMessage + + - + - + + Style="position: absolute"/> - + - + - + + Style="position: absolute"/> - + - + - + + Style="position: absolute"/> + Style="position: absolute"/> - + - + - + + Style="position: absolute"/> - + - +
@@ -73,19 +80,20 @@
@code { private SignUpModel signUpModel = new SignUpModel(); + private bool showError = false; + private string errorMessage = string.Empty; private bool popup; private async Task HandleSignUp() { - try - { - await IdentityService.SignUpAsync(signUpModel.FirstName, signUpModel.LastName, signUpModel.Email, - signUpModel.Password, "user"); + var response = await IdentityService.SignUpAsync(signUpModel.FirstName, signUpModel.LastName, signUpModel.Email, + signUpModel.Password, "user"); + if(response.ErrorMessage == null) NavigationManager.NavigateTo("/signin"); - } - catch (Exception ex) + else { - + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor similarity index 92% rename from MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor rename to MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor index 51c758c0e..305743c01 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/UpdateAccount.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/UpdateAccount.razor @@ -46,8 +46,7 @@ { if (IdentityService.IsAuthenticated) { - var userDto = await IdentityService.GetAccountAsync(); - studentDto = await StudentsService.GetStudentAsync(userDto.Id); + studentDto = StudentsService.StudentDto; } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor new file mode 100644 index 000000000..cb5c3adb4 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/AddOrganizationDialog.razor @@ -0,0 +1,53 @@ +@page "/admin/organizations/add" +@using Radzen +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Organizations +@inject IOrganizationsService OrganizationsService +@inject NavigationManager NavigationManager +@inject Radzen.DialogService DialogService + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] + public OrganizationDto NewOrganization { get; set; } + [Parameter] + public bool IsRootOrganization { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + private async void HandleAddingNewOrganization() + { + await OrganizationsService.AddOrganization(Guid.Empty, NewOrganization.Name, + IsRootOrganization ? Guid.Empty : NewOrganization.ParentId); + DialogService.Close(true); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor new file mode 100644 index 000000000..62d8380d5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/ManageStudentDialog.razor @@ -0,0 +1,208 @@ +@page "/admin/students/{StudentDto.Id}/details" +@using Radzen +@using MiniSpace.Web.DTO +@inject DialogService DialogService +@inject IIdentityService IdentityService + + + + + + + Identification + + + + + + + Id + @(StudentDto.Id) + + + + Email + @(StudentDto.Email) + + + + + + + + + + + + + + + + + Manage + + + + + + @if (StudentDto.IsBanned) + { + + } + else + { + + @if (!StudentDto.IsOrganizer) + { + + } + else + { + + } + } + + + + + + + + + + Personal info + + + + + + + First name + @(StudentDto.FirstName) + + + + Last name + @(StudentDto.LastName) + + + + + + Description + @(StudentDto.Description) + + + + Profile image + + + + + + + + + + + + + + + + + + Other info + + + + + + + Status + @(StudentDto.State) + + + + Email notifications + @(StudentDto.EmailNotifications) + + + + + + Date of birth + @(StudentDto.DateOfBirth.ToLocalTime().ToString(dateFormat)) + + + + Created at + @(StudentDto.CreatedAt.ToLocalTime().ToString(dateFormat)) + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] + public StudentDto StudentDto { get; set; } + + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + private async void Ban(StudentDto studentDto) + { + await IdentityService.BanUser(studentDto.Id); + StudentDto.IsBanned = true; + StateHasChanged(); + } + + private async void Unban(StudentDto studentDto) + { + await IdentityService.UnbanUser(studentDto.Id); + studentDto.IsBanned = false; + StateHasChanged(); + } + + private async void GrantOrganizerRights(StudentDto studentDto) + { + await IdentityService.GrantOrganizerRights(studentDto.Id); + studentDto.IsOrganizer = true; + StateHasChanged(); + } + + private async void RevokeOrganizerRights(StudentDto studentDto) + { + await IdentityService.RevokeOrganizerRights(studentDto.Id); + studentDto.IsOrganizer = false; + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor new file mode 100644 index 000000000..da04a5bc7 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/Dialogs/OrganizationDetailsDialog.razor @@ -0,0 +1,72 @@ +@page "/admin/organizations/{OrganizationId}/details" +@using Radzen +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Organizations +@inject IOrganizationsService OrganizationsService +@inject NavigationManager NavigationManager +@inject Radzen.DialogService DialogService + + + + + + + Basic info + + + + + + + Id + @(OrganizationDetailsDto.Id) + + + + Name + @(OrganizationDetailsDto.Name) + + + + + + + + + + + + + + + + + Organizers + + + + + + + + + + + + + + +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + public OrganizationDetailsDto OrganizationDetailsDto { get; set; } = new(); + private Guid selectedOrganizer; + + protected override async Task OnInitializedAsync() + { + OrganizationDetailsDto = await OrganizationsService.GetOrganizationDetailsAsync(OrganizationId); + await base.OnInitializedAsync(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor new file mode 100644 index 000000000..866304685 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageOrganizations.razor @@ -0,0 +1,161 @@ +@page "/admin/organizations" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Pages.Admin.Dialogs +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IOrganizationsService OrganizationsService +@inject NavigationManager NavigationManager + +

Manage organizations

+ +@if (!pageInitialized) +{ +

Loading...

+} +else +{ +

To manage organizers in an organization, select it and open details dialog.

+ + + + + + + + + + + + + + + @if (totalRootOrganizations == 0) + { +

There are not any organizations created.

+ } + + + + + + + false)/> + + +
+} + +@code { + private bool pageInitialized = false; + + public class ParentOrganization + { + public Guid Id { get; set; } + public string Name { get; set; } + public Guid ParentId { get; set; } + public bool IsLeaf { get; set; } + public List Children { get; set; } + } + + private int totalRootOrganizations = 0; + private List rootOrganizations = new(); + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "admin") + { + var tmpOrganizations = await OrganizationsService.GetRootOrganizationsAsync(); + ConvertOrganizationDtoList(tmpOrganizations, rootOrganizations); + totalRootOrganizations = rootOrganizations.Count; + } + + pageInitialized = true; + } + + private async void OnExpand(TreeExpandEventArgs args) + { + var parent = (ParentOrganization)args.Value; + var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(parent.Id); + ConvertOrganizationDtoList(childOrganizations, parent.Children); + StateHasChanged(); + } + + private static void ConvertOrganizationDtoList(IEnumerable input, IList result) + { + result.Clear(); + foreach (var organization in input) + { + result.Add(new ParentOrganization() + { + Id = organization.Id, + Name = organization.Name, + ParentId = organization.ParentId, + IsLeaf = organization.IsLeaf, + Children = new List() + }); + } + } + + private bool HasChildren(object org) + { + var organization = (ParentOrganization)org; + return !organization.IsLeaf; + } + + private object selectedItem; + private OrganizationDto newOrganization = new(); + + private void OnChange() + { + if (selectedItem is ParentOrganization selectedOrganization) + { + newOrganization.ParentId = selectedOrganization.Id; + } + } + + private async Task OpenAddOrganizationDialog(OrganizationDto newOrganization, bool isRootOrganization) + { + await DialogService.OpenAsync($"Add new organization:", + new Dictionary() + { + { "NewOrganization", newOrganization }, + { "IsRootOrganization", isRootOrganization } + }, + new DialogOptions() + { + Width = "500px", Height = "230px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + + if (isRootOrganization) + { + await OnInitializedAsync(); + } + else if (selectedItem is ParentOrganization selectedOrganization) + { + var childOrganizations = await OrganizationsService.GetChildrenOrganizationsAsync(selectedOrganization.Id); + ConvertOrganizationDtoList(childOrganizations, selectedOrganization.Children); + StateHasChanged(); + } + } + + private async Task OpenOrganizationDetailsDialog(ParentOrganization organization) + { + await DialogService.OpenAsync($"Details of the organization:", + new Dictionary() { { "OrganizationId", organization.Id } }, + new DialogOptions() + { + Width = "700px", Height = "600px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor new file mode 100644 index 000000000..2b404f309 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Admin/ManageStudents.razor @@ -0,0 +1,90 @@ +@page "/admin/students" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Pages.Admin.Dialogs +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService + +

Manage students

+ +@if (!pageInitialized) +{ +

Loading...

+} +@if (pageInitialized && adminId != Guid.Empty) +{ +

To manage rights of a student, open details dialog by clicking "Show" button.

+ + + @if (totalStudents == 0) + { +

There are not any students registered.

+ } + + + + + + + + + + + + + + + + + +
+} + +@code { + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private int pageSize = 5; + IEnumerable pageSizeOptions = new int[] { 5, 10, 20, 40}; + + private Guid adminId; + private bool pageInitialized = false; + + private int totalStudents = 0; + private IEnumerable students; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "admin") + { + adminId = IdentityService.GetCurrentUserId(); + + students = await StudentsService.GetStudentsAsync(); + totalStudents = students.Count(); + } + + pageInitialized = true; + } + + private async Task OpenManageStudentDialog(StudentDto studentDto) + { + await DialogService.OpenAsync($"Manage student:", + new Dictionary() { { "StudentDto", studentDto } }, + new DialogOptions() + { + Width = "700px", Height = "600px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventDetailsDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventDetailsDialog.razor new file mode 100644 index 000000000..c8a368b7e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventDetailsDialog.razor @@ -0,0 +1,185 @@ +@page "/events/{EventDto.Id}/details" +@using MiniSpace.Web.Models.Events +@using Radzen +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Events +@inject NavigationManager NavigationManager +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IEventsService EventsService + + + + + + + Basic info + + + + + + + Name + @(EventDto.Name) + + Organizer name + @(EventDto.Organizer.Name) + + Organization + @(EventDto.Organizer.OrganizationName) + + + + Description + @(EventDto.Description) + + Organizer email + @(EventDto.Organizer.Email) + + Category + @(EventDto.Category) + + + + + + + + + + + + + + + + + Dates + + + + + + + Start date + @(EventDto.StartDate.ToLocalTime().ToString(dateFormat)) + + Status + @(EventDto.Status) + + + + End date + @(EventDto.EndDate.ToLocalTime().ToString(dateFormat)) + + Publish date + @(EventDto.PublishDate.ToLocalTime().ToString(dateFormat)) + + + + + + + + + + + + + + + + + Address + + + + + + + Building name + @(EventDto.Location.BuildingName) + + Building number + @(EventDto.Location.BuildingNumber) + + City + @(EventDto.Location.City) + + + + Street + @(EventDto.Location.Street) + + Apartment number + @(EventDto.Location.ApartmentNumber) + + Zip code + @(EventDto.Location.ZipCode) + + + + + + + + + + + + + + + + + Numbers + + + + + + + Number of interested students + @(EventDto.InterestedStudents) + + Capacity + @(EventDto.Capacity) + + + + Number of signed up students + @(EventDto.SignedUpStudents) + + Fee + @(EventDto.Fee) + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] + public EventDto EventDto { get; set; } + + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor new file mode 100644 index 000000000..1f7103eb9 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsOrganizeDialog.razor @@ -0,0 +1,92 @@ +@page "/events/organize/dialog" +@using MiniSpace.Web.Models.Events +@using Radzen +@using Blazorise.DeepCloner +@inject NavigationManager NavigationManager +@inject Radzen.DialogService DialogService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] + public SearchOrganizerEventsModel SearchOrganizerEventsModel { get; set; } + + private SearchOrganizerEventsModel TempSearchOrganizerEventsModel { get; set; } + + + private List states = + [ + "", + "Published", + "Cancelled", + "Archived" + ]; + + private List directions = + [ + "Ascending", + "Descending" + ]; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + TempSearchOrganizerEventsModel = SearchOrganizerEventsModel.DeepClone(); + } + + private void HandleFiltering() + { + SearchOrganizerEventsModel.OrganizerId = TempSearchOrganizerEventsModel.OrganizerId; + SearchOrganizerEventsModel.Name = TempSearchOrganizerEventsModel.Name; + SearchOrganizerEventsModel.State = TempSearchOrganizerEventsModel.State; + SearchOrganizerEventsModel.DateFrom = TempSearchOrganizerEventsModel.DateFrom; + SearchOrganizerEventsModel.DateTo = TempSearchOrganizerEventsModel.DateTo; + SearchOrganizerEventsModel.Pageable = TempSearchOrganizerEventsModel.Pageable.DeepClone(); + + DialogService.Close(true); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor new file mode 100644 index 000000000..1e81295da --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Dialogs/EventsSearchDialog.razor @@ -0,0 +1,113 @@ +@page "/events/search/dialog" +@using MiniSpace.Web.Models.Events +@using Radzen +@using Blazorise.DeepCloner +@inject NavigationManager NavigationManager +@inject Radzen.DialogService DialogService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code { + [Parameter] + public SearchEventsModel SearchEventsModel { get; set; } + + private SearchEventsModel TempSearchEventsModel { get; set; } + + private List categories = + [ + "", + "Music", + "Sports", + "Education", + "Science", + "Technology", + "Art", + "Business", + "Health", + "Charity", + "Other" + ]; + + private List states = + [ + "", + "Published", + "Cancelled", + "Archived" + ]; + + private List directions = + [ + "Ascending", + "Descending" + ]; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + TempSearchEventsModel = SearchEventsModel.DeepClone(); + } + + private void HandleFiltering() + { + SearchEventsModel.Name = TempSearchEventsModel.Name; + SearchEventsModel.Organizer = TempSearchEventsModel.Organizer; + SearchEventsModel.Category = TempSearchEventsModel.Category; + SearchEventsModel.State = TempSearchEventsModel.State; + SearchEventsModel.DateFrom = TempSearchEventsModel.DateFrom; + SearchEventsModel.DateTo = TempSearchEventsModel.DateTo; + SearchEventsModel.Pageable = TempSearchEventsModel.Pageable.DeepClone(); + + DialogService.Close(true); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor new file mode 100644 index 000000000..5eafb9ae7 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Event.razor @@ -0,0 +1,262 @@ +@page "/events/{EventId}" +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Pages.Events.Dialogs +@using MiniSpace.Web.Areas.Posts +@using Radzen +@using AlignItems = Radzen.AlignItems +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@using Orientation = Radzen.Orientation +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IEventsService EventsService +@inject IPostsService PostsService +@inject NavigationManager NavigationManager + +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized) +{ +

@ev.Name

+ + + + + + Description + @(ev.Description) + @if (ev.Organizer != null) + { + Organization + @(ev.Organizer.OrganizationName) + Organizer + @(ev.Organizer.Name) + } + + + + + + Start date + @(ev.StartDate.ToLocalTime().ToString(dateFormat)) + Category + @(ev.Category) + + + End date + @(ev.EndDate.ToLocalTime().ToString(dateFormat)) + Fee + @(ev.Fee) + + + + + + + + + + + + + @if (IdentityService.IsAuthenticated && !IsUserEventOrganizer(ev) + && (IdentityService.GetCurrentUserRole() == "user" || IdentityService.GetCurrentUserRole() == "organizer")) + { + @if (!ev.IsSignedUp) + { + + } + else + { + + } + + @if (!ev.IsInterested) + { + + } + else + { + + } + } + + + @if (IdentityService.IsAuthenticated && IsUserEventOrganizer(ev)) + { + + + + + + + + + } + + + + + + @if (pageInitialized && !posts.Any()) + { +

No posts have been added by organizers yet.

+ } + + + +
+ + + + + + + + + +
+
+
+} + +@code { + [Parameter] + public string EventId { get; set; } + + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private Guid studentId; + public EventDto ev = new(); + private bool pageInitialized = false; + + IEnumerable posts; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + studentId = IdentityService.GetCurrentUserId(); + } + ev = await EventsService.GetEventAsync(new Guid(EventId)); + posts = await PostsService.GetPostsAsync(new Guid(EventId)); + + pageInitialized = true; + } + + private string GetAuthorName(Guid authorId) + { + return authorId == ev.Organizer.Id ? ev.Organizer.Name : "Author unknown"; + } + + private bool IsUserEventOrganizer(EventDto eventDto) + { + if (IdentityService.GetCurrentUserRole() != "organizer") + { + return false; + } + + return eventDto.Organizer != null && studentId == eventDto.Organizer.Id; + } + + private async void OnChange(int index) + { + if (!IdentityService.IsAuthenticated) + { + return; + } + + pageInitialized = false; + switch (index) + { + case 0: + posts = await PostsService.GetPostsAsync(ev.Id); + break; + } + pageInitialized = true; + } + + private async Task OpenEventDetailsDialog(EventDto eventDto) + { + await DialogService.OpenAsync($"Details of the event:", + new Dictionary() { { "EventDto", eventDto } }, + new DialogOptions() + { + Width = "700px", Height = "600px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } + + private async void SignUpToEvent(EventDto eventDto) + { + await EventsService.SignUpToEventAsync(eventDto.Id, studentId); + eventDto.IsSignedUp = true; + StateHasChanged(); + } + + private async void CancelSignUpToEvent(EventDto eventDto) + { + await EventsService.CancelSignUpToEventAsync(eventDto.Id, studentId); + eventDto.IsSignedUp = false; + StateHasChanged(); + } + + private async void ShowInterestInEvent(EventDto eventDto) + { + await EventsService.ShowInterestInEventAsync(eventDto.Id, studentId); + eventDto.IsInterested = true; + StateHasChanged(); + } + + private async void CancelInterestInEvent(EventDto eventDto) + { + await EventsService.CancelInterestInEventAsync(eventDto.Id, studentId); + eventDto.IsInterested = false; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor new file mode 100644 index 000000000..da81391ab --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventAdd.razor @@ -0,0 +1,232 @@ +@page "/events/add" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Models.Events +@using Radzen +@inject IIdentityService IdentityService +@inject IEventsService EventsService +@inject IOrganizationsService OrganizationsService +@inject IErrorMapperService ErrorMapperService +@inject NavigationManager NavigationManager + +

Add new event

+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && organizerId != Guid.Empty) +{ + + @errorMessage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (publishInfo == 2) + { + + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +@code { + private Guid organizerId; + private bool pageInitialized = false; + + private AddEventModel addEventModel = new() + { + Name = "One of first events!", + Category = "Art", + StartDate = new DateTime(2024, 05, 09), + EndDate = new DateTime(2024, 05, 11), + BuildingName = "Gmach Główny", + Street = "Plac Politechniki", + BuildingNumber = "1", + ApartmentNumber = "1", + City = "Warszawa", + ZipCode = "00-061", + Description = "Great event!", + Capacity = 30, + Fee = 34.44M + }; + private bool showError = false; + private string errorMessage = string.Empty; + private int publishInfo = 1; + + private List categories = + [ + "Music", + "Sports", + "Education", + "Science", + "Technology", + "Art", + "Business", + "Health", + "Charity", + "Other" + ]; + + private IEnumerable organizations; + + private static bool ValidateDate(DateTime dateTime) + { + return dateTime.Minute % 5 == 0; + } + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") + { + organizerId = IdentityService.GetCurrentUserId(); + organizations = await OrganizationsService.GetOrganizerOrganizationsAsync(organizerId); + + addEventModel.OrganizerId = organizerId; + addEventModel.OrganizationId = organizations.First().Id; + } + + pageInitialized = true; + } + + private async Task HandleCreateEvent() + { + var response = await EventsService.AddEventAsync(Guid.Empty, addEventModel.Name, + addEventModel.OrganizerId, addEventModel.OrganizationId, + addEventModel.StartDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + addEventModel.EndDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + addEventModel.BuildingName, addEventModel.Street, addEventModel.BuildingNumber, + addEventModel.ApartmentNumber, addEventModel.City, addEventModel.ZipCode, + addEventModel.Description, addEventModel.Capacity, addEventModel.Fee, addEventModel.Category, + publishInfo == 2 ? addEventModel.PublishDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); + + if (response.ErrorMessage != null) + { + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); + } + else + { + NavigationManager.NavigateTo("/events/organize"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor new file mode 100644 index 000000000..85ac9b694 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor @@ -0,0 +1,226 @@ +@page "/events/{EventId}/update" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Models.Events +@using Radzen +@inject IIdentityService IdentityService +@inject IEventsService EventsService +@inject IErrorMapperService ErrorMapperService +@inject NavigationManager NavigationManager + +

Update your event

+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && organizerId != Guid.Empty) +{ + + @errorMessage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (publishInfo == 2) + { + + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +@code { + [Parameter] + public string EventId { get; set; } + + private Guid organizerId; + private EventDto eventDto; + private bool pageInitialized = false; + + private UpdateEventModel updateEventModel = new(); + private bool showError = false; + private string errorMessage = string.Empty; + private int publishInfo = 1; + + private List categories = + [ + "Music", + "Sports", + "Education", + "Science", + "Technology", + "Art", + "Business", + "Health", + "Charity", + "Other" + ]; + + private static bool ValidateDate(DateTime dateTime) + { + return dateTime.Minute % 5 == 0; + } + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") + { + organizerId = IdentityService.GetCurrentUserId(); + + eventDto = await EventsService.GetEventAsync(new Guid(EventId)); + updateEventModel.EventId = eventDto.Id; + updateEventModel.Name = eventDto.Name; + updateEventModel.OrganizerId = eventDto.Organizer.Id; + updateEventModel.StartDate = eventDto.StartDate; + updateEventModel.EndDate = eventDto.EndDate; + updateEventModel.BuildingName = eventDto.Location.BuildingName; + updateEventModel.Street = eventDto.Location.Street; + updateEventModel.BuildingNumber = eventDto.Location.BuildingNumber; + updateEventModel.ApartmentNumber = eventDto.Location.ApartmentNumber; + updateEventModel.City = eventDto.Location.City; + updateEventModel.ZipCode = eventDto.Location.ZipCode; + updateEventModel.Description = eventDto.Description; + updateEventModel.Capacity = eventDto.Capacity; + updateEventModel.Fee = eventDto.Fee; + updateEventModel.Category = eventDto.Category; + } + + pageInitialized = true; + } + + private async Task HandleUpdateEvent() + { + var response = await EventsService.UpdateEventAsync(updateEventModel.EventId, + updateEventModel.Name, updateEventModel.OrganizerId, + updateEventModel.StartDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + updateEventModel.EndDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + updateEventModel.BuildingName, updateEventModel.Street, updateEventModel.BuildingNumber, + updateEventModel.ApartmentNumber, updateEventModel.City, updateEventModel.ZipCode, + updateEventModel.Description, updateEventModel.Capacity, updateEventModel.Fee, updateEventModel.Category, + publishInfo == 2 ? updateEventModel.PublishDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); + + if (response.ErrorMessage != null) + { + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); + } + else + { + NavigationManager.NavigateTo($"/events/{EventId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor new file mode 100644 index 000000000..c728a2e12 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/Events.razor @@ -0,0 +1,141 @@ +@page "/events" +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Pages.Events.Dialogs +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IEventsService EventsService +@inject NavigationManager NavigationManager + +

Follow events

+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && studentId != Guid.Empty) +{ + + + + + @if (totalSignedUpElements == 0) + { +

You haven't been signed up for any event yet.

+ } + + + +
+ + + @if (totalInterestedElements == 0) + { +

You haven't been interested in any event yet.

+ } + + + +
+
+
+
+} + +@code { + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private Guid studentId; + private bool pageInitialized = false; + + private int numberOfResults = 10; + + int totalSignedUpElements = 0; + IEnumerable signedUpEvents; + + int totalInterestedElements = 0; + IEnumerable interestedEvents; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + studentId = IdentityService.GetCurrentUserId(); + + var tmp = await EventsService.GetStudentEventsAsync(studentId, numberOfResults); + if (tmp != null) + { + signedUpEvents = tmp.Content.Where(ev => ev.IsSignedUp); + totalSignedUpElements = signedUpEvents.Count(); + + interestedEvents = tmp.Content.Where(ev => ev.IsInterested); + totalInterestedElements = interestedEvents.Count(); + } + else + { + signedUpEvents = new List(); + totalSignedUpElements = 0; + + interestedEvents = new List(); + totalInterestedElements = 0; + } + } + + pageInitialized = true; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor new file mode 100644 index 000000000..8e5ab86db --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsOrganize.razor @@ -0,0 +1,168 @@ +@page "/events/organize" +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Wrappers +@using MiniSpace.Web.Models.Events +@using MiniSpace.Web.Pages.Events.Dialogs +@using MudBlazor +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@using Orientation = Radzen.Orientation +@inject DialogService DialogService +@inject IEventsService EventsService +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager + +

Organize events

+ +
+ + + + + + +
+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && totalElements == 0) +{ +

No results found. Create new event or try to give us more general filtering criteria.

+} + +@if (pageInitialized && totalElements != 0) +{ + + + +} + + + + + +@if (pageInitialized && totalElements != 0) +{ + + + +} + +@code { + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private SearchOrganizerEventsModel searchOrganizerEventsModel = new() + { + Name = "", + State = "", + DateFrom = new DateTime(2024, 04, 14), + DateTo = new DateTime(2024, 05, 31), + Pageable = new PageableDto() + { + Page = 1, + Size = 5, + Sort = new SortDto() + { + SortBy = new List() { "dateFrom" }, + Direction = "Ascending" + } + } + }; + + private Guid organizerId; + private bool pageInitialized = false; + + int totalPages = 0; + int totalElements = 0; + IEnumerable events; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") + { + organizerId = IdentityService.GetCurrentUserId(); + searchOrganizerEventsModel.OrganizerId = organizerId; + + var tmp = await EventsService.SearchOrganizerEventsAsync(searchOrganizerEventsModel.OrganizerId, + searchOrganizerEventsModel.Name, searchOrganizerEventsModel.State, + searchOrganizerEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.Pageable); + if (tmp.Content != null) + { + totalPages = tmp.Content.TotalPages; + totalElements = tmp.Content.TotalElements; + events = tmp.Content.Content; + } + else + { + totalPages = 0; + totalElements = 0; + events = new List(); + } + } + + pageInitialized = true; + } + + private async void SelectedPageChanged(int pageNumber) + { + searchOrganizerEventsModel.Pageable.Page = pageNumber; + + var tmp = await EventsService.SearchOrganizerEventsAsync(searchOrganizerEventsModel.OrganizerId, + searchOrganizerEventsModel.Name, searchOrganizerEventsModel.State, + searchOrganizerEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchOrganizerEventsModel.Pageable); + events = tmp.Content.Content; + StateHasChanged(); + } + + private async Task OpenSearchDialog() + { + await DialogService.OpenAsync($"Filter your events by criteria:", + new Dictionary() { { "SearchOrganizerEventsModel", searchOrganizerEventsModel } }, + new DialogOptions() + { + Width = "700px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + await OnInitializedAsync(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor new file mode 100644 index 000000000..b8e45309f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventsSearch.razor @@ -0,0 +1,160 @@ +@page "/events/search" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Events +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Wrappers +@using MiniSpace.Web.Models.Events +@using MiniSpace.Web.Pages.Events.Dialogs +@using MudBlazor +@using Radzen +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@using Orientation = Radzen.Orientation +@inject DialogService DialogService +@inject IEventsService EventsService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager + +

Search events

+ +
+ +
+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && totalElements == 0) +{ +

No results found. Try to give us more general filtering criteria.

+} + +@if (pageInitialized && totalElements != 0) +{ + + + +} + + + + + +@if (pageInitialized && totalElements != 0) +{ + + + +} + +@code { + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private SearchEventsModel searchEventsModel = new() + { + Name = "", + Organizer = "", + Category = "", + State = "", + Friends = [], + FriendsEngagementType = "", + DateFrom = new DateTime(2024, 04, 14), + DateTo = new DateTime(2024, 05, 31), + Pageable = new PageableDto() + { + Page = 1, + Size = 5, + Sort = new SortDto() + { + SortBy = new List() { "dateFrom" }, + Direction = "Ascending" + } + } + }; + + private bool pageInitialized = false; + + int totalPages = 0; + int totalElements = 0; + IEnumerable events; + + protected override async Task OnInitializedAsync() + { + var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, + searchEventsModel.Organizer, searchEventsModel.Category, searchEventsModel.State, + searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, + searchEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.Pageable); + if (tmp.Content != null) + { + totalPages = tmp.Content.TotalPages; + totalElements = tmp.Content.TotalElements; + events = tmp.Content.Content; + } + else + { + totalPages = 0; + totalElements = 0; + events = new List(); + } + + pageInitialized = true; + } + + private async void SelectedPageChanged(int pageNumber) + { + searchEventsModel.Pageable.Page = pageNumber; + + var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, + searchEventsModel.Organizer, searchEventsModel.Category, searchEventsModel.State, + searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, + searchEventsModel.DateFrom.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.DateTo.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + searchEventsModel.Pageable); + events = tmp.Content.Content; + StateHasChanged(); + } + + private async Task OpenSearchDialog() + { + await DialogService.OpenAsync($"Filter all events by criteria:", + new Dictionary() { { "SearchEventsModel", searchEventsModel } }, + new DialogOptions() + { + Width = "700px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + await OnInitializedAsync(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor new file mode 100644 index 000000000..0eabfbcd7 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -0,0 +1,190 @@ +@page "/friends" +@using MiniSpace.Web.HttpClients +@using Radzen.Blazor +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.DTO; +@inject IIdentityService IdentityService +@inject IFriendsService FriendsService +@inject NavigationManager NavigationManager +@inject Radzen.NotificationService NotificationService +@inject Radzen.DialogService DialogService + +
+ +
+ @if (friends != null && friends.Any()) + { + foreach (var friend in friends) + { +
+ Friend Image +
+
@friend.StudentDetails.FirstName @friend.StudentDetails.LastName
+

@friend.StudentDetails.Email

+
+ + +
+
+
+ } + } + else + { +

No friends to show. Start connecting now!

+ } +
+
+ + + +@code { + private List friends; + private Guid studentId; + private string searchTerm; + + protected override async Task OnInitializedAsync() + { + try + { + studentId = IdentityService.GetCurrentUserId(); + var friendsResult = await FriendsService.GetAllFriendsAsync(studentId); + if (friendsResult != null) + { + friends = friendsResult.ToList(); + } + else + { + friends = new List(); + } + } + catch (Exception ex) + { + NotificationService.Notify(Radzen.NotificationSeverity.Error, "Failed to Load Friends", $"An error occurred: {ex.Message}", 5000); + friends = new List(); + } + } + + private void ViewDetails(Guid friendId) + { + NavigationManager.NavigateTo($"/student-details/{friendId}"); + } + + private async Task RemoveFriend(Guid friendId) + { + await FriendsService.RemoveFriendAsync(friendId); + friends.RemoveAll(f => f.StudentId == friendId); + + NotificationService.Notify(Radzen.NotificationSeverity.Warning, "Friend Removed", $"You have removed a friend.", 5000); + StateHasChanged(); + } + + private string GetImage(string base64Image) + { + if (string.IsNullOrWhiteSpace(base64Image)) + { + return "images/user_default.png"; + } + return $"data:image/jpeg;base64,{base64Image}"; + } + + private async Task ConfirmRemoveFriend(Guid friendId) + { + var confirm = await DialogService.Confirm("Are you sure you want to remove this friend?", "Confirm Removal", new Radzen.ConfirmOptions() { OkButtonText = "Yes", CancelButtonText = "No" }); + if (confirm.HasValue && confirm.Value) + { + await RemoveFriend(friendId); + } + } + + private async Task LoadFriends() + { + try + { + var friendsResult = await FriendsService.GetAllFriendsAsync(studentId); + if (friendsResult != null) + { + friends = friendsResult.ToList(); + } + else + { + friends = new List(); + } + } + catch (Exception ex) + { + NotificationService.Notify(Radzen.NotificationSeverity.Error, "Failed to Load Friends", $"An error occurred: {ex.Message}", 5000); + friends = new List(); + } + } + + private void SearchFriends() + { + searchTerm = searchTerm.Trim(); + if (!string.IsNullOrEmpty(searchTerm)) + { + friends = friends.Where(f => f.StudentDetails.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || f.StudentDetails.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + LoadFriends().Wait(); + } + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsDetails.razor new file mode 100644 index 000000000..b789c3b44 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsDetails.razor @@ -0,0 +1,66 @@ +@page "/student-details/{Id:guid}" +@using MiniSpace.Web.Areas.Friends +@inject IFriendsService FriendsService +@using MiniSpace.Web.DTO +@using Radzen.Blazor + +

Student Profile

+@if (student == null) +{ + +} +else +{ +
+
+ Profile Image +

@student.FirstName @student.LastName

+

@student.Email

+
+
+

Description: @student.Description

+

Number of Friends: @student.NumberOfFriends

+

Date of Birth: @student.DateOfBirth.ToString("yyyy-MM-dd")

+

State: @student.State

+

Joined: @student.CreatedAt.ToString("yyyy-MM-dd")

+
+
+} + +@code { + [Parameter] public Guid Id { get; set; } + private StudentDto student; + + protected override async Task OnInitializedAsync() + { + student = await FriendsService.GetStudentAsync(Id); + } +} + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor new file mode 100644 index 000000000..c9cfda16e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor @@ -0,0 +1,72 @@ +@page "/friends/requests" +@using MiniSpace.Web.HttpClients +@using Radzen.Blazor +@inject NavigationManager NavigationManager +@using MiniSpace.Web.Areas.Friends +@inject IFriendsService FriendsService +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Services.Friends.Application.Dto; +@inject IIdentityService IdentityService +@inject Radzen.NotificationService NotificationService + +

Incoming Friend Requests

+ +@if (incomingRequests == null) +{ +

Loading...

+} +else if (incomingRequests.Any()) +{ + + + + + + + + + + +} +else +{ +

No incoming requests.

+} + +@code { + private IEnumerable incomingRequests; + + protected override async Task OnInitializedAsync() + { + incomingRequests = await FriendsService.GetIncomingFriendRequestsAsync(); + } + + private async Task AcceptRequest(Guid requestId) + { + var request = incomingRequests.FirstOrDefault(r => r.Id == requestId); + if (request != null) + { + await FriendsService.AcceptFriendRequestAsync(request.Id, request.InviterId, request.InviteeId); + incomingRequests = incomingRequests.Where(r => r.Id != requestId).ToList(); + NotificationService.Notify(Radzen.NotificationSeverity.Success, "Request Accepted", duration: 4000); + StateHasChanged(); // Refresh the UI + } + } + + private async Task DeclineRequest(Guid requestId) + { + var request = incomingRequests.FirstOrDefault(r => r.Id == requestId); + if (request != null) + { + await FriendsService.DeclineFriendRequestAsync(request.Id, request.InviterId, request.InviteeId); + incomingRequests = incomingRequests.Where(r => r.Id != requestId).ToList(); + NotificationService.Notify(Radzen.NotificationSeverity.Warning, "Request Declined", duration: 4000); + StateHasChanged(); // Update the UI + } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor new file mode 100644 index 000000000..aa401476f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor @@ -0,0 +1,349 @@ +@page "/friends/search" +@using MiniSpace.Services.Friends.Application.Dto +@using MiniSpace.Web.HttpClients +@using Radzen.Blazor +@inject NavigationManager NavigationManager +@using MiniSpace.Web.Areas.Friends +@inject IFriendsService FriendsService +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Identity +@inject IIdentityService IdentityService +@inject Radzen.NotificationService NotificationService +@inject IJSRuntime JSRuntime + + +
+
+ +
+ @foreach (var student in students) + { +
+ Student Image +
+
@student.FirstName @student.LastName
+

Email: @student.Email

+
+ @* *@ + + @if (!sentRequests.Any(r => r.InviteeId == student.Id)) + { + + } + else + { + + } +
+
+
+ } +
+
+ +
+ + @if (student != null) + { +
+ +
+
+ +
+ Profile Image +
+

@student?.FirstName @student?.LastName

+
    +
  • ID: @student?.Id
  • +
  • Email: @student?.Email
  • +
  • Description: @student?.Description
  • +
  • Number of Friends: @student?.NumberOfFriends
  • +
  • Date of Birth: @student?.DateOfBirth.ToString("yyyy-MM-dd")
  • +
  • State: @student?.State
  • +
  • Created At: @student?.CreatedAt.ToString("yyyy-MM-dd")
  • +
+ @if (!sentRequests.Any(r => r.InviteeId == student.Id)) + { + + } + else + { + + } +
+ +
+
+ } + else + { +
+

Select a student to view details.

+ using MiniSpace.Services.Friends.Application.Dto; +
+ } +
+
+
+ + + Page @currentPage of @(Math.Ceiling((double)totalStudents / pageSize)) + + +
+ + + + +@code { + private string searchTerm; + private List students = new List(); + private IEnumerable sentRequests; + private StudentDto student; + RadzenNotification notificationComponent; + private int currentPage = 1; + private int pageSize = 10; + private int totalStudents; + + protected override async Task OnInitializedAsync() { + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + await LoadStudents(); + StateHasChanged(); + } + + @* private async Task LoadStudents() { + var result = await FriendsService.GetAllStudentsAsync(currentPage, pageSize); + totalStudents = result.Count(); + if (result != null) { + students = result.ToList(); + } + } *@ + private async Task LoadStudents() + { + int maxPage = (int)Math.Ceiling((double)totalStudents / pageSize); + if (currentPage > maxPage) currentPage = maxPage; + if (currentPage < 1) currentPage = 1; + + var response = await FriendsService.GetAllStudentsAsync(currentPage, pageSize); + if (response != null) + { + students = response.Results; + totalStudents = response.Total; + } + else + { + students = new List(); + } + StateHasChanged(); + } + + private string GetImage(string base64Image) + { + if (string.IsNullOrWhiteSpace(base64Image)) + { + return "images/user_image.png"; + } + else + { + return $"data:image/jpeg;base64,{base64Image}"; + } + } + + + private void OnDetails(StudentDto selectedStudent) + { + student = selectedStudent; + } + + private void SearchFriends() { + students = students.Where(s => s.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + s.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + @* private async Task ConnectWithStudent(Guid studentId) + { + var response = await FriendsService.AddFriendAsync(studentId); + + } *@ + + private async Task ConnectWithStudent(Guid studentId, MouseEventArgs e) + { + var currentUserId = IdentityService.GetCurrentUserId(); + await FriendsService.InviteStudent(currentUserId, studentId); + + var student = students.FirstOrDefault(s => s.Id == studentId); + if (student != null) + { + student.InvitationSent = true; + student.IsInvitationPending = true; + } + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + NotificationService.Notify(Radzen.NotificationSeverity.Success, "Invitation Sent", "The invitation has been successfully sent.", 10000); + await JSRuntime.InvokeVoidAsync("playNotificationSound"); + StateHasChanged(); + } + + private async Task SetPage(int page) + { + Console.WriteLine($"Attempting to set page to {page}"); + if (page < 1 || page > Math.Ceiling((double)totalStudents / pageSize)) { + Console.WriteLine("Page number out of range."); + return; + } + currentPage = page; + Console.WriteLine($"Page set to {currentPage}"); + await LoadStudents(); + StateHasChanged(); + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor new file mode 100644 index 000000000..41c341e52 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor @@ -0,0 +1,52 @@ +@page "/friends/sent-requests" +@using MiniSpace.Web.HttpClients +@using Radzen.Blazor +@inject NavigationManager NavigationManager +@using MiniSpace.Web.Areas.Friends +@inject IFriendsService FriendsService +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Services.Friends.Application.Dto; +@inject IIdentityService IdentityService +@inject Radzen.NotificationService NotificationService + +

Sent Friend Requests

+ +@if (sentRequests == null) +{ +

Loading...

+} +else if (sentRequests.Any()) +{ + + + + + + + + + + +} +else +{ +

No sent requests.

+} + +@code { + private IEnumerable sentRequests; + + protected override async Task OnInitializedAsync() + { + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + StateHasChanged(); + } + + private void RedirectToDetails(Guid id) + { + NavigationManager.NavigateTo($"/student-details/{id}"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor index f6185d7e9..305d6fd31 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor @@ -1,6 +1,8 @@ @page "/greeting" @using Microsoft.AspNetCore.Components.Authorization @inject IIdentityService IdentityService +@using Blazored.LocalStorage +@inject ILocalStorageService localStorage

Greeting

@@ -17,13 +19,5 @@ else @code { private string email; - protected override async Task OnInitializedAsync() - { - 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 e12ff0b12..fb16ed110 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor @@ -1,12 +1,10 @@ @page "/" @using MudBlazor - - - + @foreach (var img in images) { @@ -15,13 +13,34 @@ } + + @foreach (var img in images2) + { + + +
+
+
+ } +
- + @* Mini Space @titles[activeIndex] @descriptions[activeIndex] Get Started - + *@ +
+

Welcome to Mini Space

+

@titles[activeIndex]

+

@descriptions[activeIndex]

+ +
+
@@ -50,6 +69,52 @@
+ + @code { private int activeIndex = 0; private bool arrows = true; @@ -58,6 +123,7 @@ private bool autocycle = true; private MudBlazor.Transition transition = MudBlazor.Transition.Slide; private List images = new List { "images/mini_1.jpg", "images/pw_1.jpg", "images/pw_2.jpg" }; + private List images2 = new List { "images/students_1.jpg", "images/students_2.jpg", "images/students_3.jpg" }; private List titles = new List { "Exploration", "Connection", "Sharing" }; private List descriptions = new List { "Explore new places and meet new people on MiniSpace.", diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Dialogs/DeletePostDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Dialogs/DeletePostDialog.razor new file mode 100644 index 000000000..f97e4e36b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Dialogs/DeletePostDialog.razor @@ -0,0 +1,35 @@ +@page "/events/{EventId}/posts/{PostId}/author/{AuthorName}/delete" +@using MiniSpace.Web.Areas.Posts +@using Radzen +@inject DialogService DialogService +@inject IPostsService PostsService +@inject NavigationManager NavigationManager + + + + + + + + + + + +@code { + [Parameter] + public Guid EventId { get; set; } + [Parameter] + public Guid PostId { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + private async void DeletePost(Guid postId) + { + await PostsService.DeletePostAsync(postId); + DialogService.Close(true); + NavigationManager.NavigateTo($"/events/{EventId}"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor new file mode 100644 index 000000000..bd813320e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/Post.razor @@ -0,0 +1,132 @@ +@page "/events/{EventId}/posts/{PostId}/author/{AuthorName}" +@using Microsoft.Extensions.Logging +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Areas.Posts +@using MiniSpace.Web.Pages.Posts.Dialogs +@using Radzen +@using AlignItems = Radzen.AlignItems +@using DialogOptions = Radzen.DialogOptions +@using DialogService = Radzen.DialogService +@using Orientation = Radzen.Orientation +@inject DialogService DialogService +@inject IIdentityService IdentityService +@inject IPostsService PostsService +@inject NavigationManager NavigationManager + +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized) +{ + + + + + + + @if (post.UpdatedAt != null) + { + + Updated at @(post.UpdatedAt?.ToLocalTime().ToString(dateFormat)) + + } + else + { + + Published at @(post.CreatedAt.ToLocalTime().ToString(dateFormat)) + + } + + + +
+ + + + + @(post.TextContent) + + + +
+ + + + + + + + @if (IdentityService.IsAuthenticated && IsUserPostCreator(post)) + { + + + + + } + + + + + + + + + + + +
+} + +@code { + [Parameter] + public string EventId { get; set; } + [Parameter] + public string PostId { get; set; } + [Parameter] + public string AuthorName { get; set; } + + private const string dateFormat = "dd/MM/yyyy HH:mm"; + + private Guid studentId; + private PostDto post = new(); + private bool pageInitialized = false; + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated) + { + studentId = IdentityService.GetCurrentUserId(); + } + post = await PostsService.GetPostAsync(new Guid(PostId)); + + pageInitialized = true; + } + + private bool IsUserPostCreator(PostDto postDto) + { + if (IdentityService.GetCurrentUserRole() != "organizer") + { + return false; + } + + return studentId == postDto.OrganizerId; + } + + private async Task OpenDeletePostDialog(Guid postId) + { + await DialogService.OpenAsync("Are you sure? This action cannot be undone!", + new Dictionary() { {"EventId", new Guid(EventId) }, { "PostId", new Guid(PostId) } }, + new DialogOptions() + { + Width = "500px", Height = "100px", Resizable = true, Draggable = true, + AutoFocusFirstElement = false + }); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor new file mode 100644 index 000000000..29865e975 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor @@ -0,0 +1,114 @@ +@page "/events/{EventId}/posts/create" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.Posts +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Models.Posts +@using Radzen +@inject IIdentityService IdentityService +@inject IPostsService PostsService +@inject IErrorMapperService ErrorMapperService +@inject NavigationManager NavigationManager + +

Create new post

+ + + @errorMessage + + + + + + + + + + + + + + + + + + + + + + @if (publishInfo == 2) + { + + + + + } + + + + + + + + + + + + + + + +@code { + [Parameter] + public string EventId { get; set; } + + private Guid organizerId; + + private CreatePostModel createPostModel = new() + { + TextContent = "Lorem ipsum!", + MediaContent = "" + }; + private bool showError = false; + private string errorMessage = string.Empty; + private int publishInfo = 1; + + private static bool ValidateDate(DateTime dateTime) + { + return dateTime.Minute % 5 == 0; + } + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") + { + organizerId = IdentityService.GetCurrentUserId(); + + createPostModel.EventId = new Guid(EventId); + createPostModel.OrganizerId = organizerId; + } + } + + private async Task HandleCreatePost() + { + var response = await PostsService.CreatePostAsync(Guid.Empty, createPostModel.EventId, + createPostModel.OrganizerId, createPostModel.TextContent, createPostModel.MediaContent, + publishInfo == 2 ? "ToBePublished" : "Published", + publishInfo == 2 ? createPostModel.PublishDate : null); + + if (response.ErrorMessage != null) + { + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); + } + else + { + NavigationManager.NavigateTo($"/events/{EventId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor new file mode 100644 index 000000000..e0fac9bec --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor @@ -0,0 +1,106 @@ +@page "/events/{EventId}/posts/{PostId}/author/{AuthorName}/update" +@using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.Posts +@using MiniSpace.Web.DTO +@using MiniSpace.Web.Models.Posts +@using Radzen +@inject IIdentityService IdentityService +@inject IPostsService PostsService +@inject IErrorMapperService ErrorMapperService +@inject NavigationManager NavigationManager + +

Update your post

+ +@if (!pageInitialized) +{ +

Loading...

+} + +@if (pageInitialized && organizerId != Guid.Empty) +{ + + @errorMessage + + + + + + + + + + + + + + + + + + + + + + + + + +} + +@code { + [Parameter] + public string EventId { get; set; } + [Parameter] + public string PostId { get; set; } + [Parameter] + public string AuthorName { get; set; } + + private Guid organizerId; + private PostDto postDto; + private bool pageInitialized = false; + + private UpdatePostModel updatePostModel = new(); + private bool showError = false; + private string errorMessage = string.Empty; + private int publishInfo = 1; + + private static bool ValidateDate(DateTime dateTime) + { + return dateTime.Minute % 5 == 0; + } + + protected override async Task OnInitializedAsync() + { + if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") + { + organizerId = IdentityService.GetCurrentUserId(); + + postDto = await PostsService.GetPostAsync(new Guid(PostId)); + updatePostModel.PostId = postDto.Id; + updatePostModel.TextContent = postDto.TextContent; + updatePostModel.MediaContent = postDto.MediaContent; + } + + pageInitialized = true; + } + + private async Task HandleUpdatePost() + { + var response = await PostsService.UpdatePostAsync(updatePostModel.PostId, + updatePostModel.TextContent, updatePostModel.MediaContent); + + if (response.ErrorMessage != null) + { + showError = true; + errorMessage = ErrorMapperService.MapError(response.ErrorMessage); + } + else + { + NavigationManager.NavigateTo($"events/{EventId}/posts/{PostId}/author/{AuthorName}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml index 70def0688..fa3c2f0a2 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml @@ -42,6 +42,12 @@ scroll(); } + function playNotificationSound() { + var audio = new Audio('sounds/create-connection.mp3'); + audio.play(); + } + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor new file mode 100644 index 000000000..09940b9df --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor @@ -0,0 +1,79 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Radzen +@using MiniSpace.Web.Areas.Students +@inherits LayoutComponentBase +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject NavigationManager NavigationManager +@inject Blazored.LocalStorage.ILocalStorageService localStorage + + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
+
+ + +
+ + + @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") + { + + } +
+ +
+
+ + +
+ +
+
+
+
+
+ @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") + { + + + + + + + + + } + +
+ @Body +
+
+
+ +
+ + +@code { + bool _sidebarExpanded = true; + + public bool IsUserAuthenticated() => IdentityService.IsAuthenticated; + + void NavigateToHome() { + NavigationManager.NavigateTo("/home"); + } + + async Task SignOut() { + await localStorage.RemoveItemAsync("accessToken"); + await localStorage.RemoveItemAsync("jwtDto"); + NavigationManager.NavigateTo("signin", forceLoad: true); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor index ff47836cb..85d038000 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor @@ -1,7 +1,7 @@ @using Radzen @inherits LayoutComponentBase - +
About MiniSpace
diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/DynamicLayoutComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/DynamicLayoutComponent.razor new file mode 100644 index 000000000..90b0e30aa --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/DynamicLayoutComponent.razor @@ -0,0 +1,37 @@ +@* @inherits LayoutComponentBase +@using Microsoft.AspNetCore.Components.Authorization +@inject AuthenticationStateProvider AuthenticationStateProvider + + + @if (_layoutComponent != null) + { + @(_layoutComponent) + } + + +@code { + private RenderFragment _layoutComponent; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity.IsAuthenticated) + { + _layoutComponent = builder => + { + builder.OpenComponent(0, typeof(AuthenticatedLayout)); + builder.CloseComponent(); + }; + } + else + { + _layoutComponent = builder => + { + builder.OpenComponent(0, typeof(NotAuthenticatedLayout)); + builder.CloseComponent(); + }; + } + } + + +} *@ diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor index 232330fa4..2dbc18f34 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor @@ -1,71 +1,169 @@ @using Radzen +@using MiniSpace.Web.Areas.Students @inherits LayoutComponentBase @inject IIdentityService IdentityService +@inject IStudentsService StudentsService @inject NavigationManager NavigationManager @inject Microsoft.JSInterop.IJSRuntime JSRuntime +@using Blazored.LocalStorage +@inject ILocalStorageService localStorage +@inject AuthenticationStateProvider authenticationStateProvider - -
- - - @if (IdentityService.IsAuthenticated) - { - - } -
- -
-
- -
- - - - @if (IdentityService.IsAuthenticated) - { - - } - else + +
+ + + @* + TODO: do not forget about the code snippet chaned ❕❕❕ + @if (IsUserAuthenticated && StudentsService.StudentDto.State == "valid") + *@ + @if (_isUserAuthenticated ) { - - + } -
-
-
+
+ +
+ + +
+ + @if (_isUserAuthenticated) + { + +
+ + + + + + + + +
+ + } + else + { + + + + + + } +
+
+
- - @if (IdentityService.IsAuthenticated) - { - - - - - - - } - - -
- @Body -
-
+ +
+ @* + TODO: do not forget about the code snippet chaned ❕❕❕ + @if (IsUserAuthenticated && StudentsService.StudentDto.State == "valid") + *@ + @if (IsUserAuthenticated) + { + + + + + + + + + + @if (IdentityService.GetCurrentUserRole() == "organizer") + { + + } + + + + + + + + + + @if (IdentityService.GetCurrentUserRole() == "admin") + { + + + + + } + + + } + + +
+ @Body +
+
+
+
+ + @code{ bool _sidebarExpanded = true; + bool isLoading = true; + private bool firstRender = true; + private string _userName; + private string _userSurname; + + private string _userEmail; + + private bool _isUserAuthenticated; + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) { + _isUserAuthenticated = await IdentityService.CheckIfUserIsAuthenticated(); + if (_isUserAuthenticated) { + var userDto = await IdentityService.GetAccountAsync(IdentityService.JwtDto); + if (userDto != null) { + _userName = userDto.Name; + _userEmail = userDto.Email; + @* _userSurname = userDto.LastName; *@ + } + } + StateHasChanged(); + } + } - void SignOut() + public async Task CheckAuthentication() { - IdentityService.Logout(); - NavigationManager.NavigateTo(""); + _isUserAuthenticated = await IdentityService.CheckIfUserIsAuthenticated(); + Console.WriteLine($"IsUserAuthenticated: {_isUserAuthenticated}"); + return _isUserAuthenticated; } - - async Task ScrollToSection(string sectionId) + + public bool IsUserAuthenticated => _isUserAuthenticated; + async Task SignOut() + { + Console.WriteLine("Signing out..."); + await localStorage.RemoveItemAsync("accessToken"); + await localStorage.RemoveItemAsync("jwtDto"); + NavigationManager.NavigateTo("signin", forceLoad: true); + } + + async Task ScrollToSection(string sectionId) { if (NavigationManager.Uri != NavigationManager.BaseUri) { @@ -82,4 +180,5 @@ { NavigationManager.NavigateTo("/"); } + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor index 033fad61b..f5af1b7e8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/NavMenu.razor @@ -1,8 +1,10 @@ @* @inject HttpClient Http *@ @inject IIdentityService IdentityService +@inherits LayoutComponentBase @inject NavigationManager NavigationManager +@inject Blazored.LocalStorage.ILocalStorageService LocalStorage - *@ @code { private bool isUserAuthenticated; @@ -37,9 +39,19 @@ protected override async Task OnInitializedAsync() { - NavigationManager.LocationChanged += LocationChanged; - isUserAuthenticated = IdentityService.IsAuthenticated; + @* isUserAuthenticated = await LocalStorage.GetItemAsync("isAuthenticated"); + NavigationManager.LocationChanged += HandleLocationChanged; *@ + } + + private void HandleLocationChanged(object sender, LocationChangedEventArgs e) + { + @* InvokeAsync(() => + { + isUserAuthenticated = LocalStorage.GetItemAsync("isAuthenticated").Result; + StateHasChanged(); + }); *@ } + string NavMenuCssClass => collapseNavMenu ? "collapse" : null; void ToggleNavMenu() @@ -47,8 +59,10 @@ collapseNavMenu = !collapseNavMenu; } - void LocationChanged(object sender, LocationChangedEventArgs e) + @* void LocationChanged(object sender, LocationChangedEventArgs e) { InvokeAsync(StateHasChanged); - } -} \ No newline at end of file + } *@ +} + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor new file mode 100644 index 000000000..34f8a1255 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor @@ -0,0 +1,50 @@ +@using Radzen +@using MiniSpace.Web.Areas.Students +@inherits LayoutComponentBase +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager +@inject Microsoft.JSInterop.IJSRuntime JSRuntime + + +
+ + +
+ +
+
+ +
+ + + + + +
+
+
+
+ +
+ @Body +
+
+ +
+ + +@code { + void NavigateToHome() { + NavigationManager.NavigateTo("/"); + } + + async Task ScrollToSection(string sectionId) { + if (NavigationManager.Uri != NavigationManager.BaseUri) { + NavigationManager.NavigateTo("/", true); + } + while (NavigationManager.Uri != NavigationManager.BaseUri) { + await Task.Delay(100); + } + await JSRuntime.InvokeVoidAsync("scrollToSection", sectionId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index 39216dc8a..09227b190 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -10,13 +10,19 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MiniSpace.Web.Areas.Events; +using MiniSpace.Web.Areas.Http; //using MiniSpace.Web.Data; using MiniSpace.Web.Models.Identity; using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Areas.Organizations; using MiniSpace.Web.Areas.Posts; using MiniSpace.Web.Areas.Students; using MiniSpace.Web.HttpClients; +using MudBlazor; using MudBlazor.Services; +using MiniSpace.Web.Areas.Friends; +using Microsoft.AspNetCore.Components.Authorization; +using Blazored.LocalStorage; namespace MiniSpace.Web { @@ -39,20 +45,30 @@ public void ConfigureServices(IServiceCollection services) var httpClientOptions = Configuration.GetSection("HttpClientOptions").Get(); - // Register HttpClientOptions as a singleton services.AddSingleton(httpClientOptions); - // Register IHttpClient to resolve to CustomHttpClient services.AddHttpClient((serviceProvider, client) => { var options = serviceProvider.GetRequiredService(); client.BaseAddress = new Uri(options.ApiUrl); }); + services.AddBlazoredLocalStorage(); + + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -69,6 +85,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseHsts(); } + app.UseAuthentication(); + app.UseAuthorization(); app.UseHttpsRedirection(); app.UseStaticFiles(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor index b9dad2fbc..6f2381777 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor @@ -10,4 +10,6 @@ @using MiniSpace.Web.Models.Identity @using MiniSpace.Web.Areas.Identity @using Radzen.Blazor +@using MiniSpace.Web.Areas.Identity + diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css index a8e4d9090..9508286cf 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css @@ -169,6 +169,7 @@ header { flex-wrap: wrap; align-items: center; justify-content: space-between; + } @@ -210,17 +211,17 @@ main { } .landing-page { - padding: 2rem; + /* padding: 2rem; */ } @media (min-width: 768px) { .landing-page { - padding: 2rem 5rem; + /* padding: 2rem 5rem; */ } } @media (min-width: 992px) { .landing-page { - padding: 2rem 10rem; + /* padding: 2rem 10rem; */ } } @@ -262,9 +263,49 @@ html, body { } } +.rz-menu .rz-profile-menu .rz-navigation-item-link { + margin-top: -10px !important; +} + +.rz-navigation-menu { + background-color: #2F4E6F !important; + color: #fff !important; + transition: background-color 0.3s ease; +} + +.rz-navigation-item-text { + color: #fff !important; + transition: color 0.3s ease; +} + +.rz-navigation-menu a { + display: block; + text-decoration: none; + color: #fff; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.rz-navigation-menu a:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #98d1ff; + text-decoration: none; +} + +.rz-navigation-menu a:active, +.rz-navigation-menu a:focus { + background-color: rgba(255, 255, 255, 0.2); + color: #aad4ff; +} + +.rz-navigation-menua a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + .rz-menu { background-color: #2F4E6F !important; color: #fff !important; + height: 50px; } @@ -307,9 +348,9 @@ html, body { padding: 8px 0; } - .rz-display-flex { - display: block !important; - } + /*.rz-display-flex {*/ + /* display: block !important;*/ + /*}*/ } .RadzenStack { @@ -342,3 +383,12 @@ html, body { display: flex; gap: 20px; } + + +/*.rz-body {*/ +/* padding: 0 !important;*/ +/*}*/ + +/*.rz-layout .rz-body {*/ +/* overflow: unset !important;*/ +/*}*/ \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.jpg b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.jpg new file mode 100644 index 000000000..cb3f12cbd Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_1.jpg differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.jpg b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.jpg new file mode 100644 index 000000000..4f3ce854e Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_2.jpg differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.jpg b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.jpg new file mode 100644 index 000000000..95a4e9a7d Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/students_3.jpg differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/user_image.png b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/user_image.png new file mode 100644 index 000000000..2d9fbf692 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/user_image.png differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection.mp3 b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection.mp3 new file mode 100644 index 000000000..b43215680 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/sounds/create-connection.mp3 differ diff --git a/MiniSpace/compose/infrastructure.yml b/MiniSpace/compose/infrastructure.yml index bb4fff88f..0ad902612 100644 --- a/MiniSpace/compose/infrastructure.yml +++ b/MiniSpace/compose/infrastructure.yml @@ -22,7 +22,7 @@ services: - minispace ports: - 9998:9998 - - 9999:9999 + - 10000:9999 grafana: image: grafana/grafana @@ -98,7 +98,7 @@ services: networks: - minispace ports: - - 6379:6379 + - 6380:6379 volumes: - redis:/data diff --git a/MiniSpace/compose/services.yml b/MiniSpace/compose/services.yml index a4b7dfd6a..86000600e 100644 --- a/MiniSpace/compose/services.yml +++ b/MiniSpace/compose/services.yml @@ -38,7 +38,52 @@ services: - 5008:80 networks: - minispace - + + comments-service: + image: adrianvsaint/minispace.services.comments:latest + container_name: comments-service + restart: unless-stopped + ports: + - 5009:80 + networks: + - minispace + + reactions-service: + image: adrianvsaint/minispace.services.reactions:latest + container_name: reactions-service + restart: unless-stopped + ports: + - 5010:80 + networks: + - minispace + + friends-service: + image: adrianvsaint/minispace.services.friends:latest + container_name: friends-service + restart: unless-stopped + ports: + - 5012:80 + networks: + - minispace + + posts-service: + image: adrianvsaint/minispace.services.posts:latest + container_name: posts-service + restart: unless-stopped + ports: + - 5013:80 + networks: + - minispace + + organizations-service: + image: adrianvsaint/minispace.services.organizations:latest + container_name: organizations-service + restart: unless-stopped + ports: + - 5015:80 + networks: + - minispace + web: image: adrianvsaint/minispace.web:latest container_name: web diff --git a/MiniSpace/scripts/dockerize-all.sh b/MiniSpace/scripts/dockerize-all.sh index 586c9d274..f2889fe36 100755 --- a/MiniSpace/scripts/dockerize-all.sh +++ b/MiniSpace/scripts/dockerize-all.sh @@ -6,6 +6,11 @@ directories=( "MiniSpace.Services.Identity" "MiniSpace.Services.Events" "MiniSpace.Services.Students" + "MiniSpace.Services.Friends" + "MiniSpace.Services.Reactions" + "MiniSpace.Services.Posts" + "MiniSpace.Services.Comments" + "MiniSpace.Services.Organizations" ) for dir in "${directories[@]}"; do