diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e143e8650..784c9e352 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: @SaintAngeLs -patreon: patreon.com/SaintAngeLs +github: SaintAngeLs +patreon: SaintAngeLs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..36de2dbe4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + target-branch: "dev" + open-pull-requests-limit: 5 diff --git a/.github/settings.yml b/.github/settings.yml index c46f44e1a..e23ae7984 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -8,7 +8,7 @@ repository: name: distributed_minispace # A short description of the repository that will show up on GitHub - description: "Microservices-based social network built with .NET8 and ASP .NET Core Blazor Server, employing DDD principles." + description: "Microservices-based social network built with .NET8 and ASP .NET Core Blazor Server, employing DDD principles. Offers a scalable and efficient technical solution for social media applications enginine, leveraging CQRS, MongoDB, SignalR, .NET ML, Elasticsearch and event-driven architecture to ensure high performance and reliability." # A URL with more information about the repository homepage: https://minispace.itsharppro.com @@ -113,11 +113,8 @@ milestones: state: open # Collaborators: give specific users access to this repository. -collaborators: - - username: eggwhat - permission: push - - username: an2508374 - permission: push +# collaborators: + # Note: `permission` is only valid on organization-owned repositories. # The permission to grant the collaborator. Can be one of: diff --git a/.github/workflows/build_microservices.yml b/.github/workflows/build_microservices.yml index c0b18b144..4295518e5 100644 --- a/.github/workflows/build_microservices.yml +++ b/.github/workflows/build_microservices.yml @@ -15,21 +15,21 @@ jobs: include: - project: 'MiniSpace.APIGateway/src/MiniSpace.APIGateway' - project: 'MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api' - test_dir: 'MiniSpace.Services.Identity/tests' - - project: 'MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api' - test_dir: 'MiniSpace.Services.Posts/tests' + # test_dir: 'MiniSpace.Services.Identity/tests' - project: 'MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api' - test_dir: 'MiniSpace.Services.Comments/tests' - # - project: 'MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api' - # test_dir: 'MiniSpace.Services.Organizations/tests' + # test_dir: 'MiniSpace.Services.Comments/tests' + - project: 'MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api' + # test_dir: 'MiniSpace.Services.Organizations/tests' - project: 'MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api' - test_dir: 'MiniSpace.Services.Posts/tests' + # test_dir: 'MiniSpace.Services.Posts/tests' + - project: 'MiniSpace.Services.Email/src/MiniSpace.Services.Email.Api' + - project: 'MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api' - project: 'MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api' - test_dir: 'MiniSpace.Services.Reactions/tests' + # test_dir: 'MiniSpace.Services.Reactions/tests' - project: 'MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api' - test_dir: 'MiniSpace.Services.Events/tests' + # test_dir: 'MiniSpace.Services.Events/tests' - project: 'MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api' - test_dir: 'MiniSpace.Services.Students/tests' + # test_dir: 'MiniSpace.Services.Students/tests' - project: 'MiniSpace.Web/src/MiniSpace.Web' steps: - uses: actions/checkout@v3 diff --git a/MiniSpace.APIGateway/scripts/start.sh b/MiniSpace.APIGateway/scripts/start.sh index 2df375d6f..2d79cb22e 100755 --- a/MiniSpace.APIGateway/scripts/start.sh +++ b/MiniSpace.APIGateway/scripts/start.sh @@ -1,4 +1,4 @@ #!/bin/bash export ASPNETCORE_ENVIRONMENT=local -cd src/MiniSpace.APIGateway +cd ../src/MiniSpace.APIGateway dotnet run diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/.gitignore b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml index bb5ee68ae..524cadb34 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.docker.yml @@ -41,7 +41,7 @@ extensions: - Total-Count jwt: - issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij + issuerSigningKey: Gtn9vBDB5RCDLJSMqZQQmN75J8hgzbQwWkcD8jMIXnvCLAmlL0QVacUAbyootWihMrPIz validIssuer: minispace validateAudience: false validateIssuer: true @@ -106,13 +106,13 @@ modules: auth: true claims: role: admin - + - upstream: /me method: GET use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -121,25 +121,13 @@ modules: resourceId: property: userId generate: true - + - upstream: /sign-in method: POST 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 @@ -188,17 +176,29 @@ modules: downstream: identity-service/2fa/generate-secret auth: true + - upstream: /2fa/verify-code + method: POST + use: downstream + downstream: identity-service/2fa/verify-code + auth: false + + - upstream: /access-tokens/revoke + method: POST + use: downstream + downstream: identity-service/access-tokens/revoke + auth: true + - upstream: /refresh-tokens/use method: POST use: downstream downstream: identity-service/refresh-tokens/use - auth: false + auth: true - upstream: /refresh-tokens/revoke method: POST use: downstream downstream: identity-service/refresh-tokens/revoke - auth: false + auth: true services: identity-service: @@ -328,12 +328,6 @@ modules: url: notifications-service - - - - - - students: path: /students routes: @@ -349,24 +343,48 @@ modules: downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId} - method: PUT + - upstream: /{studentId}/settings + method: GET use: downstream - downstream: students-service/students/{studentId} - bind: - - studentId:{studentId} + downstream: students-service/students/{studentId}/settings + auth: true + + - upstream: /{studentId}/gallery + method: GET + use: downstream + downstream: students-service/students/{studentId}/gallery + auth: true + + - upstream: /{studentId}/visibility-settings + method: GET + use: downstream + downstream: students-service/students/{studentId}/visibility-settings + auth: true + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + - upstream: /{studentId}/notifications + method: GET + use: downstream + downstream: students-service/students/{studentId}/notifications auth: true - upstream: /{studentId} - method: DELETE + method: PUT use: downstream downstream: students-service/students/{studentId} + bind: + - studentId:{studentId} auth: true - - upstream: / - method: POST + - upstream: /{studentId}/settings + method: PUT use: downstream - downstream: students-service/students + downstream: students-service/students/{studentId}/settings auth: true - upstream: /{studentId}/state/{state} @@ -377,19 +395,17 @@ modules: - studentId:{studentId} - state:{state} auth: true - claims: - role: admin - - upstream: /{studentId}/events - method: GET + - upstream: /{studentId} + method: DELETE use: downstream - downstream: students-service/students/{studentId}/events + downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId}/notifications - method: GET + - upstream: / + method: POST use: downstream - downstream: students-service/students/{studentId}/notifications + downstream: students-service/students auth: true - upstream: /{studentId}/notifications diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml index 017e20d1c..dc47eaf91 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml @@ -41,7 +41,7 @@ extensions: - Total-Count jwt: - issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij + issuerSigningKey: Gtn9vBDB5RCDLJSMqZQQmN75J8hgzbQwWkcD8jMIXnvCLAmlL0QVacUAbyootWihMrPIz validIssuer: minispace validateAudience: false validateIssuer: true @@ -106,13 +106,13 @@ modules: auth: true claims: role: admin - + - upstream: /me method: GET use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -121,25 +121,13 @@ modules: resourceId: property: userId generate: true - + - upstream: /sign-in method: POST 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 @@ -188,17 +176,29 @@ modules: downstream: identity-service/2fa/generate-secret auth: true + - upstream: /2fa/verify-code + method: POST + use: downstream + downstream: identity-service/2fa/verify-code + auth: false + + - upstream: /access-tokens/revoke + method: POST + use: downstream + downstream: identity-service/access-tokens/revoke + auth: true + - upstream: /refresh-tokens/use method: POST use: downstream downstream: identity-service/refresh-tokens/use - auth: false + auth: true - upstream: /refresh-tokens/revoke method: POST use: downstream downstream: identity-service/refresh-tokens/revoke - auth: false + auth: true services: identity-service: @@ -327,12 +327,6 @@ modules: url: notifications-service - - - - - - students: path: /students routes: @@ -348,24 +342,48 @@ modules: downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId} - method: PUT + - upstream: /{studentId}/settings + method: GET use: downstream - downstream: students-service/students/{studentId} - bind: - - studentId:{studentId} + downstream: students-service/students/{studentId}/settings + auth: true + + - upstream: /{studentId}/gallery + method: GET + use: downstream + downstream: students-service/students/{studentId}/gallery + auth: true + + - upstream: /{studentId}/visibility-settings + method: GET + use: downstream + downstream: students-service/students/{studentId}/visibility-settings + auth: true + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + - upstream: /{studentId}/notifications + method: GET + use: downstream + downstream: students-service/students/{studentId}/notifications auth: true - upstream: /{studentId} - method: DELETE + method: PUT use: downstream downstream: students-service/students/{studentId} + bind: + - studentId:{studentId} auth: true - - upstream: / - method: POST + - upstream: /{studentId}/settings + method: PUT use: downstream - downstream: students-service/students + downstream: students-service/students/{studentId}/settings auth: true - upstream: /{studentId}/state/{state} @@ -376,19 +394,17 @@ modules: - studentId:{studentId} - state:{state} auth: true - claims: - role: admin - - upstream: /{studentId}/events - method: GET + - upstream: /{studentId} + method: DELETE use: downstream - downstream: students-service/students/{studentId}/events + downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId}/notifications - method: GET + - upstream: / + method: POST use: downstream - downstream: students-service/students/{studentId}/notifications + downstream: students-service/students auth: true - upstream: /{studentId}/notifications diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml index f834ef7ff..01d674117 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml @@ -43,7 +43,7 @@ extensions: - Total-Count jwt: - issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij + issuerSigningKey: Gtn9vBDB5RCDLJSMqZQQmN75J8hgzbQwWkcD8jMIXnvCLAmlL0QVacUAbyootWihMrPIz validIssuer: minispace validateAudience: false validateIssuer: false @@ -83,13 +83,13 @@ modules: auth: true claims: role: admin - + - upstream: /me method: GET use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -98,25 +98,13 @@ modules: resourceId: property: userId generate: true - + - upstream: /sign-in method: POST 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 @@ -165,17 +153,29 @@ modules: downstream: identity-service/2fa/generate-secret auth: true + - upstream: /2fa/verify-code + method: POST + use: downstream + downstream: identity-service/2fa/verify-code + auth: false + + - upstream: /access-tokens/revoke + method: POST + use: downstream + downstream: identity-service/access-tokens/revoke + auth: true + - upstream: /refresh-tokens/use method: POST use: downstream downstream: identity-service/refresh-tokens/use - auth: false + auth: true - upstream: /refresh-tokens/revoke method: POST use: downstream downstream: identity-service/refresh-tokens/revoke - auth: false + auth: true services: identity-service: @@ -302,12 +302,6 @@ modules: notifications-service: localUrl: localhost:5006 url: notifications-service - - - - - - students: @@ -325,24 +319,48 @@ modules: downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId} - method: PUT + - upstream: /{studentId}/settings + method: GET use: downstream - downstream: students-service/students/{studentId} - bind: - - studentId:{studentId} + downstream: students-service/students/{studentId}/settings + auth: true + + - upstream: /{studentId}/gallery + method: GET + use: downstream + downstream: students-service/students/{studentId}/gallery + auth: true + + - upstream: /{studentId}/visibility-settings + method: GET + use: downstream + downstream: students-service/students/{studentId}/visibility-settings + auth: true + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + - upstream: /{studentId}/notifications + method: GET + use: downstream + downstream: students-service/students/{studentId}/notifications auth: true - upstream: /{studentId} - method: DELETE + method: PUT use: downstream downstream: students-service/students/{studentId} + bind: + - studentId:{studentId} auth: true - - upstream: / - method: POST + - upstream: /{studentId}/settings + method: PUT use: downstream - downstream: students-service/students + downstream: students-service/students/{studentId}/settings auth: true - upstream: /{studentId}/state/{state} @@ -353,19 +371,17 @@ modules: - studentId:{studentId} - state:{state} auth: true - claims: - role: admin - - upstream: /{studentId}/events - method: GET + - upstream: /{studentId} + method: DELETE use: downstream - downstream: students-service/students/{studentId}/events + downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId}/notifications - method: GET + - upstream: / + method: POST use: downstream - downstream: students-service/students/{studentId}/notifications + downstream: students-service/students auth: true - upstream: /{studentId}/notifications diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index fda5d6091..bce8c883a 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -43,7 +43,7 @@ extensions: - Total-Count jwt: - issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij + issuerSigningKey: Gtn9vBDB5RCDLJSMqZQQmN75J8hgzbQwWkcD8jMIXnvCLAmlL0QVacUAbyootWihMrPIz validIssuer: minispace validateAudience: false validateIssuer: false @@ -85,13 +85,13 @@ modules: auth: true claims: role: admin - + - upstream: /me method: GET use: downstream downstream: identity-service/me auth: true - + - upstream: /sign-up method: POST use: downstream @@ -100,25 +100,13 @@ modules: resourceId: property: userId generate: true - + - upstream: /sign-in method: POST 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 @@ -160,24 +148,36 @@ modules: use: downstream downstream: identity-service/2fa/disable auth: true - + - upstream: /2fa/generate-secret method: POST use: downstream downstream: identity-service/2fa/generate-secret auth: true + - upstream: /2fa/verify-code + method: POST + use: downstream + downstream: identity-service/2fa/verify-code + auth: false + + - upstream: /access-tokens/revoke + method: POST + use: downstream + downstream: identity-service/access-tokens/revoke + auth: true + - upstream: /refresh-tokens/use method: POST use: downstream downstream: identity-service/refresh-tokens/use - auth: false + auth: true - upstream: /refresh-tokens/revoke method: POST use: downstream downstream: identity-service/refresh-tokens/revoke - auth: false + auth: true services: identity-service: @@ -306,12 +306,6 @@ modules: url: notifications-service - - - - - - students: path: /students routes: @@ -327,25 +321,51 @@ modules: downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId} - method: PUT + - upstream: /{studentId}/settings + method: GET use: downstream - downstream: students-service/students/{studentId} - bind: - - studentId:{studentId} + downstream: students-service/students/{studentId}/settings + auth: true + + - upstream: /{studentId}/gallery + method: GET + use: downstream + downstream: students-service/students/{studentId}/gallery + auth: true + + - upstream: /{studentId}/visibility-settings + method: GET + use: downstream + downstream: students-service/students/{studentId}/visibility-settings + auth: true + + - upstream: /{studentId}/events + method: GET + use: downstream + downstream: students-service/students/{studentId}/events + auth: true + + - upstream: /{studentId}/notifications + method: GET + use: downstream + downstream: students-service/students/{studentId}/notifications auth: true - upstream: /{studentId} - method: DELETE + method: PUT use: downstream downstream: students-service/students/{studentId} + bind: + - studentId:{studentId} auth: true - - upstream: / - method: POST + - upstream: /{studentId}/settings + method: PUT use: downstream - downstream: students-service/students + downstream: students-service/students/{studentId}/settings auth: true + bind: + - studentId:{studentId} - upstream: /{studentId}/state/{state} method: PUT @@ -355,19 +375,17 @@ modules: - studentId:{studentId} - state:{state} auth: true - claims: - role: admin - - upstream: /{studentId}/events - method: GET + - upstream: /{studentId} + method: DELETE use: downstream - downstream: students-service/students/{studentId}/events + downstream: students-service/students/{studentId} auth: true - - upstream: /{studentId}/notifications - method: GET + - upstream: / + method: POST use: downstream - downstream: students-service/students/{studentId}/notifications + downstream: students-service/students auth: true - upstream: /{studentId}/notifications @@ -376,6 +394,14 @@ modules: downstream: students-service/students/{studentId}/notifications auth: true + - upstream: /{studentId}/languages-and-interests + method: PUT + use: downstream + downstream: students-service/students/{studentId}/languages-and-interests + bind: + - studentId:{studentId} + auth: true + services: students-service: localUrl: localhost:5007 @@ -774,6 +800,10 @@ modules: use: downstream downstream: mediafiles-service/media-files auth: true + requestSizeLimit: 50_000_000 # Example: Limit to 50 MB + formLimits: + multipartBodyLengthLimit: 50_000_000 # Optional: Same as above to ensure large files are allowed + - upstream: /{mediaFileId} method: GET @@ -807,6 +837,54 @@ modules: downstream: organizations-service/organizations auth: true + - 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: /{organizationId}/children/all + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/children/all + + - upstream: /users/{userId}/organizations + method: GET + use: downstream + downstream: organizations-service/users/{userId}/organizations + auth: true + bind: + - userId: {userId} + + - upstream: /{organizationId}/details/gallery-users + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/details/gallery-users + bind: + - organizationId: {organizationId} + + - upstream: /{organizationId}/roles + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/roles + auth: true + bind: + - organizationId: {organizationId} + - upstream: /{organizationId} method: DELETE use: downstream @@ -818,48 +896,59 @@ modules: use: downstream downstream: organizations-service/organizations/{organizationId}/children auth: true - - - upstream: /{organizationId}/organizer + + - upstream: /{organizationId}/roles method: POST use: downstream - downstream: organizations-service/organizations/{organizationId}/organizer + downstream: organizations-service/organizations/{organizationId}/roles auth: true - - - upstream: /{organizationId}/organizer/{organizerId} - method: DELETE + + - upstream: /{organizationId}/roles/{roleId}/permissions + method: PUT use: downstream - downstream: organizations-service/organizations/{organizationId}/organizer/{organizerId} + downstream: organizations-service/organizations/{organizationId}/roles/{roleId}/permissions auth: true - - - upstream: /{organizationId} - method: GET + + - upstream: /{organizationId}/invite + method: POST use: downstream - downstream: organizations-service/organizations/{organizationId} - - - upstream: /{organizationId}/details - method: GET + downstream: organizations-service/organizations/{organizationId}/invite + auth: true + + - upstream: /{organizationId}/roles/{memberId} + method: POST use: downstream - downstream: organizations-service/organizations/{organizationId}/details + downstream: organizations-service/organizations/{organizationId}/roles/{memberId} + auth: true - - upstream: /root - method: GET + - upstream: /{organizationId}/privacy + method: POST use: downstream - downstream: organizations-service/organizations/root - - - upstream: /{organizationId}/children - method: GET + downstream: organizations-service/organizations/{organizationId}/privacy + auth: true + + - upstream: /{organizationId}/settings + method: PUT use: downstream - downstream: organizations-service/organizations/{organizationId}/children + downstream: organizations-service/organizations/{organizationId}/settings + auth: true - - upstream: /{organizationId}/children/all - method: GET + - upstream: /{organizationId}/visibility + method: PUT use: downstream - downstream: organizations-service/organizations/{organizationId}/children/all + downstream: organizations-service/organizations/{organizationId}/visibility + auth: true - - upstream: /organizer/{organizerId} - method: GET + - upstream: /{organizationId}/feed + method: PUT + use: downstream + downstream: organizations-service/organizations/{organizationId}/feed + auth: true + + - upstream: /{organizationId} + method: PUT use: downstream - downstream: organizations-service/organizations/organizer/{organizerId} + downstream: organizations-service/organizations/{organizationId} auth: true services: @@ -867,3 +956,4 @@ modules: localUrl: localhost:5015 url: organizations-service + diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/.gitignore b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Api/.gitignore copy b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Api/.gitignore copy new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Api/.gitignore copy @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Application/Events/External/Handlers/NotificationCreatedHandler.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Application/Events/External/Handlers/NotificationCreatedHandler.cs index 223284423..cae119ba2 100644 --- a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Application/Events/External/Handlers/NotificationCreatedHandler.cs +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Application/Events/External/Handlers/NotificationCreatedHandler.cs @@ -40,7 +40,6 @@ public async Task HandleAsync(NotificationCreated @event, CancellationToken canc string jsonEvent = JsonSerializer.Serialize(@event); _logger.LogInformation($"Received Event: {jsonEvent}"); - var student = await _studentsServiceClient.GetAsync(@event.UserId); if (student == null) { @@ -76,12 +75,22 @@ public async Task HandleAsync(NotificationCreated @event, CancellationToken canc await _messageBroker.PublishAsync(new EmailQueued(emailNotification.EmailNotificationId, @event.UserId)); } - private async Task LoadHtmlTemplate(string filePath, NotificationCreated eventDetails, StudentDto student) { string htmlContent = await System.IO.File.ReadAllTextAsync(filePath); - htmlContent = htmlContent.Replace("{Message}", eventDetails.Message); - htmlContent = htmlContent.Replace("{Details}", eventDetails.Details ?? "No details provided"); + var eventType = (NotificationEventType)Enum.Parse(typeof(NotificationEventType), eventDetails.EventType); + htmlContent = htmlContent.Replace("{Message}", EmailContentFactory.CreateContent(eventType, eventDetails.Details)); + + // Replace {Details} with empty string if eventType is TwoFactorCodeGenerated + if (eventType == NotificationEventType.TwoFactorCodeGenerated) + { + htmlContent = htmlContent.Replace("{Details}", string.Empty); + } + else + { + htmlContent = htmlContent.Replace("{Details}", eventDetails.Details ?? "No details provided"); + } + htmlContent = htmlContent.Replace("{CreatedAt}", eventDetails.CreatedAt.ToString("dddd, dd MMMM yyyy")); var eventTypeDescription = EmailSubjectFactory.CreateSubject( @@ -102,9 +111,7 @@ private async Task LoadHtmlTemplate(string filePath, NotificationCreated htmlContent = htmlContent.Replace("{UserEmailConsent}", userEmailConsentMessage); - return htmlContent; } - } } diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentFactory.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentFactory.cs new file mode 100644 index 000000000..01b73db47 --- /dev/null +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Email.Core.Entities +{ + public class EmailContentFactory + { + private static readonly Dictionary Strategies = + new Dictionary + { + { NotificationEventType.TwoFactorCodeGenerated, new TwoFactorCodeGeneratedContent() }, + }; + + public static string CreateContent(NotificationEventType eventType, string details) + { + if (Strategies.TryGetValue(eventType, out var strategy)) + { + return strategy.GenerateContent(details); + } + var defaultStrategy = new DefaultEmailContentStrategy(); + return defaultStrategy.GenerateContent(details); + } + } +} diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentStrategies.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentStrategies.cs new file mode 100644 index 000000000..1c8388ec1 --- /dev/null +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailContentStrategies.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace MiniSpace.Services.Email.Core.Entities +{ + public class TwoFactorCodeGeneratedContent : IEmailContentStrategy + { + public string GenerateContent(string details) + { + var code = ExtractCode(details); + if (string.IsNullOrEmpty(code)) + { + return "

Invalid 2FA code provided.

"; + } + var formattedCode = string.Join(" ", code.Select(c => $"{c}")); + return $"

Your Two-Factor Authentication Code is:

{formattedCode}
"; + } + + private string ExtractCode(string details) + { + var match = Regex.Match(details, @"\b\d{6}\b"); + return match.Success ? match.Value : string.Empty; + } + } + + public class DefaultEmailContentStrategy : IEmailContentStrategy + { + public string GenerateContent(string details) + { + return $"

{details}

"; + } + } +} diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailSubjectFactory.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailSubjectFactory.cs index 7b04fe834..338fd246e 100644 --- a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailSubjectFactory.cs +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/EmailSubjectFactory.cs @@ -32,6 +32,8 @@ public class EmailSubjectFactory { NotificationEventType.ReportReviewStarted, new ReportReviewStartedSubject() }, { NotificationEventType.NewEventInvitation, new NewEventInvitationSubject() }, { NotificationEventType.ReportCancelled, new ReportCancelledSubject() }, + { NotificationEventType.EmailVerified, new EmailVerifiedSubject() }, + { NotificationEventType.TwoFactorCodeGenerated, new TwoFactorCodeGeneratedSubject() }, { NotificationEventType.Other, new OtherSubject() } }; diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/IEmailContentStrategy.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/IEmailContentStrategy.cs new file mode 100644 index 000000000..87891e94e --- /dev/null +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/IEmailContentStrategy.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Email.Core.Entities +{ + public interface IEmailContentStrategy + { + string GenerateContent(string details); + } +} diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/NotificationEventType.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/NotificationEventType.cs index 16ec2cb64..4b789740d 100644 --- a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/NotificationEventType.cs +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/NotificationEventType.cs @@ -30,6 +30,8 @@ public enum NotificationEventType ReportReviewStarted, ReportCancelled, NewEventInvitation, + EmailVerified, + TwoFactorCodeGenerated, Other } } \ No newline at end of file diff --git a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/SubjectStrategies.cs b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/SubjectStrategies.cs index 9c87fff3a..d02a9b180 100644 --- a/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/SubjectStrategies.cs +++ b/MiniSpace.Services.Email/src/MiniSpace.Services.Email.Core/Entities/SubjectStrategies.cs @@ -134,6 +134,11 @@ public class NewEventInvitationSubject : IEmailSubjectStrategy public string GenerateSubject(string details) => "You're invited to a new event!"; } + public class EmailVerifiedSubject : IEmailSubjectStrategy + { + public string GenerateSubject(string details) => "Your email has been verified!"; + } + public class ReportCancelledSubject : IEmailSubjectStrategy { public string GenerateSubject(string details) @@ -141,4 +146,9 @@ public string GenerateSubject(string details) return $"A report has been cancelled!"; } } + + public class TwoFactorCodeGeneratedSubject : IEmailSubjectStrategy + { + public string GenerateSubject(string details) => "Your Two-Factor Authentication Code"; + } } \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/.gitignore b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/.gitignore b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs index f0f2149c6..04323e506 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs @@ -69,16 +69,6 @@ public static async Task Main(string[] args) await ctx.RequestServices.GetService().RevokeAsync(cmd.RefreshToken); ctx.Response.StatusCode = 204; }) - .Post("users/{userId}/organizer-rights", async (cmd, ctx) => - { - await ctx.RequestServices.GetService().GrantOrganizerRightsAsync(cmd); - ctx.Response.StatusCode = 204; - }) - .Delete("users/{userId}/organizer-rights", async (cmd, ctx) => - { - await ctx.RequestServices.GetService().RevokeOrganizerRightsAsync(cmd); - ctx.Response.StatusCode = 204; - }) .Post("users/{userId}/ban", async (cmd, ctx) => { await ctx.RequestServices.GetService().BanUserAsync(cmd); @@ -119,6 +109,11 @@ public static async Task Main(string[] args) var secret = await ctx.RequestServices.GetService().GenerateTwoFactorSecretAsync(cmd); await ctx.Response.WriteJsonAsync(new { Secret = secret }); }) + .Post("2fa/verify-code", async (cmd, ctx) => + { + var token = await ctx.RequestServices.GetService().VerifyTwoFactorCodeAsync(cmd); + await ctx.Response.WriteJsonAsync(token); + }) )) .UseLogging() .Build() diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs deleted file mode 100644 index a6c4a04dc..000000000 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/GrantOrganizerRights.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Identity.Application.Commands -{ - public class GrantOrganizerRights(Guid userId) : ICommand - { - public Guid UserId { get; } = userId; - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs deleted file mode 100644 index c43cd6305..000000000 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/RevokeOrganizerRights.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Identity.Application.Commands -{ - public class RevokeOrganizerRights : ICommand - { - public Guid UserId { get; } - - public RevokeOrganizerRights(Guid userId) - { - UserId = userId; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/VerifyTwoFactorCode.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/VerifyTwoFactorCode.cs new file mode 100644 index 000000000..e73fdc6a6 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/VerifyTwoFactorCode.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class VerifyTwoFactorCode : ICommand + { + public Guid UserId { get; } + public string Code { get; } + + public VerifyTwoFactorCode(Guid userId, string code) + { + UserId = userId; + Code = code ?? throw new ArgumentNullException(nameof(code)); + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs index 309eb3d85..736294d90 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Identity.Application.DTO @@ -9,5 +10,7 @@ public class AuthDto public string RefreshToken { get; set; } public string Role { get; set; } public long Expires { get; set; } + public bool IsTwoFactorRequired { get; set; } + public Guid UserId { get; set; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs index cf86a66d3..d687cabf5 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs @@ -11,7 +11,7 @@ public class UserDto public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } - public string Role { get; set; } + public Role Role { get; set; } public DateTime CreatedAt { get; set; } public IEnumerable Permissions { get; set; } public bool IsEmailVerified { get; set; } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedIn.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedIn.cs index 551bf0e13..659982b4a 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedIn.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedIn.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using MiniSpace.Services.Identity.Core.Entities; namespace MiniSpace.Services.Identity.Application.Events { @@ -7,9 +8,9 @@ namespace MiniSpace.Services.Identity.Application.Events public class SignedIn : IEvent { public Guid UserId { get; } - public string Role { get; } + public Role Role { get; } - public SignedIn(Guid userId, string role) + public SignedIn(Guid userId, Role role) { UserId = userId; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs index 9b715488c..f20e67432 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/SignedUp.cs @@ -1,5 +1,6 @@ using System; using Convey.CQRS.Events; +using MiniSpace.Services.Identity.Core.Entities; namespace MiniSpace.Services.Identity.Application.Events { diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/TwoFactorCodeGenerated.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/TwoFactorCodeGenerated.cs new file mode 100644 index 000000000..b56040317 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Events/TwoFactorCodeGenerated.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Identity.Application.Events +{ + public class TwoFactorCodeGenerated : IEvent + { + public Guid UserId { get; } + public string Code { get; } + + public TwoFactorCodeGenerated(Guid userId, string code) + { + UserId = userId; + Code = code ?? throw new ArgumentNullException(nameof(code)); + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Exceptions/InvalidTwoFactorCodeException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Exceptions/InvalidTwoFactorCodeException.cs new file mode 100644 index 000000000..0d2ada901 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Exceptions/InvalidTwoFactorCodeException.cs @@ -0,0 +1,16 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Identity.Application.Exceptions +{ + [ExcludeFromCodeCoverage] + public class InvalidTwoFactorCodeException : AppException + { + public override string Code { get; } = "invalid_two_factor_code"; + + public InvalidTwoFactorCodeException() + : base("Invalid or incorrect two-factor authentication code provided.") + { + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs index f7de1ed96..f92eebbf9 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs @@ -10,8 +10,6 @@ public interface IIdentityService Task GetAsync(Guid id); Task SignInAsync(SignIn command); Task SignUpAsync(SignUp command); - Task GrantOrganizerRightsAsync(GrantOrganizerRights command); - Task RevokeOrganizerRightsAsync(RevokeOrganizerRights command); Task BanUserAsync(BanUser command); Task UnbanUserAsync(UnbanUser command); Task ForgotPasswordAsync(ForgotPassword command); @@ -21,5 +19,6 @@ public interface IIdentityService Task EnableTwoFactorAsync(EnableTwoFactor command); Task DisableTwoFactorAsync(DisableTwoFactor command); Task GenerateTwoFactorSecretAsync(GenerateTwoFactorSecret command); + Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IJwtProvider.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IJwtProvider.cs index a86a9d750..c5e3fb974 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IJwtProvider.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IJwtProvider.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using MiniSpace.Services.Identity.Application.DTO; +using MiniSpace.Services.Identity.Core.Entities; namespace MiniSpace.Services.Identity.Application.Services { public interface IJwtProvider { - AuthDto Create(Guid userId, string role, string audience = null, + AuthDto Create(Guid userId, Role role, string audience = null, IDictionary> claims = null); string GenerateResetToken(Guid userId); diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/ITwoFactorCodeService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/ITwoFactorCodeService.cs new file mode 100644 index 000000000..533dde0b5 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/ITwoFactorCodeService.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Services.Identity.Application.Services +{ + public interface ITwoFactorCodeService + { + string GenerateCode(string secret); + bool ValidateCode(string secret, string code); + } +} 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 e9d70faaa..3a2b16074 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 @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -30,12 +31,16 @@ public class IdentityService : IIdentityService private readonly IMessageBroker _messageBroker; private readonly IVerificationTokenService _verificationTokenService; private readonly ITwoFactorSecretTokenService _twoFactorSecretTokenService; + private readonly ITwoFactorCodeService _twoFactorCodeService; private readonly ILogger _logger; public IdentityService(IUserRepository userRepository, IPasswordService passwordService, IJwtProvider jwtProvider, IRefreshTokenService refreshTokenService, IMessageBroker messageBroker, IUserResetTokenRepository userResetTokenRepository, - IVerificationTokenService verificationTokenService, ITwoFactorSecretTokenService twoFactorSecretTokenService, ILogger logger) + IVerificationTokenService verificationTokenService, + ITwoFactorSecretTokenService twoFactorSecretTokenService, + ITwoFactorCodeService twoFactorCodeService, + ILogger logger) { _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); _passwordService = passwordService ?? throw new ArgumentNullException(nameof(passwordService)); @@ -45,6 +50,7 @@ public IdentityService(IUserRepository userRepository, IPasswordService password _userResetTokenRepository = userResetTokenRepository ?? throw new ArgumentNullException(nameof(userResetTokenRepository)); _verificationTokenService = verificationTokenService ?? throw new ArgumentNullException(nameof(verificationTokenService)); _twoFactorSecretTokenService = twoFactorSecretTokenService ?? throw new ArgumentNullException(nameof(twoFactorSecretTokenService)); + _twoFactorCodeService = twoFactorCodeService ?? throw new ArgumentNullException(nameof(twoFactorCodeService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -58,21 +64,25 @@ public async Task SignInAsync(SignIn command) { if (!EmailRegex.IsMatch(command.Email)) { - _logger.LogError($"Invalid email: {command.Email}"); throw new InvalidEmailException(command.Email); } var user = await _userRepository.GetAsync(command.Email); if (user is null || !_passwordService.IsValid(user.Password, command.Password)) { - _logger.LogError($"User with email: {command.Email} was not found."); throw new InvalidCredentialsException(command.Email); } - if (!_passwordService.IsValid(user.Password, command.Password)) + if (user.IsTwoFactorEnabled) { - _logger.LogError($"Invalid password for user with id: {user.Id}"); - throw new InvalidCredentialsException(command.Email); + var code = _twoFactorCodeService.GenerateCode(user.TwoFactorSecret); + await _messageBroker.PublishAsync(new TwoFactorCodeGenerated(user.Id, code)); + + return new AuthDto + { + IsTwoFactorRequired = true, + UserId = user.Id + }; } var claims = new Dictionary> @@ -87,12 +97,13 @@ public async Task SignInAsync(SignIn command) var auth = _jwtProvider.Create(user.Id, user.Role, claims: claims); auth.RefreshToken = await _refreshTokenService.CreateAsync(user.Id); - _logger.LogInformation($"User with id: {user.Id} has been authenticated."); await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); return auth; } + + public async Task SignUpAsync(SignUp command) { if (!EmailRegex.IsMatch(command.Email)) @@ -108,12 +119,11 @@ public async Task SignUpAsync(SignUp command) throw new EmailInUseException(command.Email); } - var role = string.IsNullOrWhiteSpace(command.Role) ? "user" : command.Role.ToLowerInvariant(); + var role = string.IsNullOrWhiteSpace(command.Role) ? Role.User : Enum.Parse(command.Role, true); var password = _passwordService.Hash(command.Password); user = new User(command.UserId, $"{command.FirstName} {command.LastName}", command.Email, password, role, DateTime.UtcNow, command.Permissions); - // Generate email verification token and hashed token var (token, hashedToken) = _verificationTokenService.GenerateToken(user.Id, user.Email); user.SetEmailVerificationToken(hashedToken); @@ -122,39 +132,7 @@ public async Task SignUpAsync(SignUp command) _logger.LogInformation($"Created an account for the user with id: {user.Id}."); await _messageBroker.PublishAsync(new SignedUp(user.Id, command.FirstName, command.LastName, - user.Email, user.Role, token, hashedToken)); - } - - public async Task GrantOrganizerRightsAsync(GrantOrganizerRights command) - { - var user = await _userRepository.GetAsync(command.UserId); - if (user is null) - { - _logger.LogError($"User with id: {command.UserId} was not found."); - throw new UserNotFoundException(command.UserId); - } - - user.GrantOrganizerRights(); - await _userRepository.UpdateAsync(user); - - _logger.LogInformation($"Granted organizer rights to the user with id: {user.Id}."); - await _messageBroker.PublishAsync(new OrganizerRightsGranted(user.Id)); - } - - public async Task RevokeOrganizerRightsAsync(RevokeOrganizerRights command) - { - var user = await _userRepository.GetAsync(command.UserId); - if (user is null) - { - _logger.LogError($"User with id: {command.UserId} was not found."); - throw new UserNotFoundException(command.UserId); - } - - user.RevokeOrganizerRights(); - await _userRepository.UpdateAsync(user); - - _logger.LogInformation($"Revoked organizer rights from the user with id: {user.Id}."); - await _messageBroker.PublishAsync(new OrganizerRightsRevoked(user.Id)); + user.Email, user.Role.ToString(), token, hashedToken)); } public async Task BanUserAsync(BanUser command) @@ -309,5 +287,36 @@ public async Task GenerateTwoFactorSecretAsync(GenerateTwoFactorSecret c return secret; } + + public async Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user == null) + { + throw new UserNotFoundException(command.UserId); + } + + bool isValidCode = _twoFactorCodeService.ValidateCode(user.TwoFactorSecret, command.Code); + + if (!isValidCode) + { + throw new InvalidTwoFactorCodeException(); + } + + 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); + + await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); + return auth; + } } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs index 6373f3673..d763ef1c0 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/Role.cs @@ -1,22 +1,19 @@ +using System; + namespace MiniSpace.Services.Identity.Core.Entities { - public static class Role + public enum Role { - public const string User = "user"; - public const string Admin = "admin"; - public const string Banned = "banned"; - public const string Organizer = "organizer"; + User, + Admin, + Banned + } - public static bool IsValid(string role) + public static class RoleExtensions + { + public static bool IsValid(this Role role) { - if (string.IsNullOrWhiteSpace(role)) - { - return false; - } - - role = role.ToLowerInvariant(); - - return role == User || role == Admin || role == Banned || role == Organizer; + return Enum.IsDefined(typeof(Role), role); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/ServiceResponse.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/ServiceResponse.cs new file mode 100644 index 000000000..ad161935a --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/ServiceResponse.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Identity.Core.Entities +{ + public class ServiceResponse + { + public bool Success { get; set; } + public T Data { get; set; } + public string ErrorMessage { get; set; } + + public static ServiceResponse SuccessResponse(T data) => new ServiceResponse { Success = true, Data = data }; + public static ServiceResponse FailureResponse(string errorMessage) => new ServiceResponse { Success = false, ErrorMessage = errorMessage }; + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs index 03c2976b0..09e8d1fb1 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs @@ -9,7 +9,7 @@ public class User : AggregateRoot { public string Name { get; private set; } public string Email { get; private set; } - public string Role { get; private set; } + public Role Role { get; private set; } public string Password { get; set; } public DateTime CreatedAt { get; private set; } public IEnumerable Permissions { get; private set; } @@ -19,7 +19,7 @@ public class User : AggregateRoot public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } - public User(Guid id, string name, string email, string password, string role, DateTime createdAt, + public User(Guid id, string name, string email, string password, Role role, DateTime createdAt, IEnumerable permissions = null) { if (string.IsNullOrWhiteSpace(name)) @@ -37,21 +37,21 @@ public User(Guid id, string name, string email, string password, string role, Da throw new InvalidPasswordException(); } - if (!Entities.Role.IsValid(role)) + if (!role.IsValid()) { - throw new InvalidRoleException(role); + throw new InvalidRoleException(role.ToString()); } Id = id; Name = name; Email = email.ToLowerInvariant(); Password = password; - Role = role.ToLowerInvariant(); + Role = role; CreatedAt = createdAt; Permissions = permissions ?? Enumerable.Empty(); } - internal User(Guid id, string name, string email, string password, string role, DateTime createdAt, + internal User(Guid id, string name, string email, string password, Role role, DateTime createdAt, bool isEmailVerified, string emailVerificationToken, DateTime? emailVerifiedAt, bool isTwoFactorEnabled, string twoFactorSecret, IEnumerable permissions = null) : this(id, name, email, password, role, createdAt, permissions) @@ -62,45 +62,25 @@ internal User(Guid id, string name, string email, string password, string role, IsTwoFactorEnabled = isTwoFactorEnabled; TwoFactorSecret = twoFactorSecret; } - - public void GrantOrganizerRights() - { - if (Role != Entities.Role.User) - { - throw new UserCannotBecomeAnOrganizerException(Id, Role); - } - - Role = Entities.Role.Organizer; - } - - public void RevokeOrganizerRights() - { - if (Role != Entities.Role.Organizer) - { - throw new UserIsNotAnOrganizerException(Id); - } - - Role = Entities.Role.User; - } - + public void Ban() { - if (Role == Entities.Role.Banned || Role == Entities.Role.Admin) + if (Role == Role.Banned || Role == Role.Admin) { - throw new UserCannotBeBannedException(Id, Role); + throw new UserCannotBeBannedException(Id, Role.ToString()); } - Role = Entities.Role.Banned; + Role = Role.Banned; } public void Unban() { - if (Role != Entities.Role.Banned) + if (Role != Role.Banned) { - throw new UserIsNotBannedException(Id, Role); + throw new UserIsNotBannedException(Id, Role.ToString()); } - Role = Entities.Role.User; + Role = Role.User; } public void SetEmailVerificationToken(string token) @@ -157,4 +137,6 @@ public static class UserPermissions { public static string OrganizeEvents { get; private set; } = "organize_events"; } + + } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs deleted file mode 100644 index b69237795..000000000 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBecomeAnOrganizerException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace MiniSpace.Services.Identity.Core.Exceptions -{ - public class UserCannotBecomeAnOrganizerException : DomainException - { - public override string Code { get; } = "user_cannot_become_an_organizer"; - public Guid UserId { get; } - public string Role { get; } - - public UserCannotBecomeAnOrganizerException(Guid userId, string role) : base($"User with ID: {userId} and role: {role} is cannot become an organizer.") - { - UserId = userId; - Role = role; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs deleted file mode 100644 index c4987758c..000000000 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotAnOrganizerException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace MiniSpace.Services.Identity.Core.Exceptions -{ - public class UserIsNotAnOrganizerException : DomainException - { - public override string Code { get; } = "user_is_not_an_organizer"; - public Guid UserId { get; } - - public UserIsNotAnOrganizerException(Guid userId) : base($"User with ID: {userId} is not an organizer.") - { - UserId = userId; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/JwtProvider.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/JwtProvider.cs index f00f41798..109ede39b 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/JwtProvider.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/JwtProvider.cs @@ -4,6 +4,7 @@ using Convey.Auth; using MiniSpace.Services.Identity.Application.DTO; using MiniSpace.Services.Identity.Application.Services; +using MiniSpace.Services.Identity.Core.Entities; namespace MiniSpace.Services.Identity.Infrastructure.Auth { @@ -17,10 +18,11 @@ public JwtProvider(IJwtHandler jwtHandler) _jwtHandler = jwtHandler; } - public AuthDto Create(Guid userId, string role, string audience = null, + public AuthDto Create(Guid userId, Role role, string audience = null, IDictionary> claims = null) { - var jwt = _jwtHandler.CreateToken(userId.ToString("N"), role, audience, claims); + var roleString = role.ToString(); + var jwt = _jwtHandler.CreateToken(userId.ToString("N"), roleString, audience, claims); return new AuthDto { @@ -42,4 +44,4 @@ public string GenerateResetToken(Guid userId) return jwt.AccessToken; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/TwoFactorCodeService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/TwoFactorCodeService.cs new file mode 100644 index 000000000..8e7ad3b08 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Auth/TwoFactorCodeService.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using MiniSpace.Services.Identity.Application.Services; + +namespace MiniSpace.Services.Identity.Infrastructure.Auth +{ + public class TwoFactorCodeService : ITwoFactorCodeService + { + private const int Step = 30; // Time step in seconds + private const int TOTPSize = 6; // Number of digits in the TOTP + private const int Window = 1; // Allowable time step window for clock drift + + public bool ValidateCode(string secret, string code) + { + long unixTime = GetUnixTimestamp(); + byte[] secretBytes = Base32ToBytes(secret); + + + for (int i = -Window; i <= Window; i++) + { + int otp = ComputeTotp(secretBytes, (unixTime / Step) + i); + + if (otp.ToString("D6") == code) + { + return true; + } + } + + return false; + } + + public string GenerateCode(string secret) + { + byte[] secretBytes = Base32ToBytes(secret); + long unixTime = GetUnixTimestamp(); + int otp = ComputeTotp(secretBytes, unixTime / Step); + return otp.ToString("D6"); + } + + private static long GetUnixTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + public static int ComputeTotp(byte[] key, long counter) + { + using (var hmac = new HMACSHA1(key)) + { + byte[] counterBytes = BitConverter.GetBytes(counter); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(counterBytes); + } + + byte[] hash = hmac.ComputeHash(counterBytes); + int offset = hash[hash.Length - 1] & 0xf; + int binary = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff); + + int otp = binary % (int)Math.Pow(10, TOTPSize); + return otp; + } + } + + public static byte[] Base32ToBytes(string base32) + { + const string base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + base32 = base32.TrimEnd('=').ToUpper(); + byte[] outputBytes = new byte[base32.Length * 5 / 8]; + + byte curByte = 0, bitsRemaining = 8; + int mask = 0, arrayIndex = 0; + + foreach (char c in base32) + { + int cValue = base32Chars.IndexOf(c); + + if (cValue < 0) + { + throw new ArgumentException("Invalid base32 character", nameof(base32)); + } + + if (bitsRemaining > 5) + { + mask = cValue << (bitsRemaining - 5); + curByte |= (byte)mask; + bitsRemaining -= 5; + } + else + { + mask = cValue >> (5 - bitsRemaining); + curByte |= (byte)mask; + outputBytes[arrayIndex++] = curByte; + curByte = (byte)(cValue << (3 + bitsRemaining)); + bitsRemaining += 3; + } + } + + if (arrayIndex != outputBytes.Length) + { + outputBytes[arrayIndex] = curByte; + } + + return outputBytes; + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Extensions.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Extensions.cs index 260553d9a..2c57a3081 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Extensions.cs @@ -60,6 +60,7 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddSingleton, PasswordHasher>(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton, PasswordHasher>(); builder.Services.AddSingleton(); @@ -154,5 +155,7 @@ internal static string GetSpanContext(this IMessageProperties messageProperties, return string.Empty; } + + } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs index 20f1f4616..8b6729bd1 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs @@ -10,8 +10,10 @@ namespace MiniSpace.Services.Identity.Infrastructure.Mongo.Documents internal static class Extensions { public static User AsEntity(this UserDocument document) - => new User(document.Id, document.Name, document.Email, document.Password, document.Role, document.CreatedAt, - document.Permissions) + => new User(document.Id, document.Name, document.Email, document.Password, + Enum.Parse(document.Role, true), document.CreatedAt, + document.Permissions + ) { IsEmailVerified = document.IsEmailVerified, EmailVerificationToken = document.EmailVerificationToken, @@ -27,7 +29,7 @@ public static UserDocument AsDocument(this User entity) Name = entity.Name, Email = entity.Email, Password = entity.Password, - Role = entity.Role, + Role = entity.Role.ToString(), CreatedAt = entity.CreatedAt, Permissions = entity.Permissions ?? Enumerable.Empty(), IsEmailVerified = entity.IsEmailVerified, @@ -43,7 +45,7 @@ public static UserDto AsDto(this UserDocument document) Id = document.Id, Name = document.Name, Email = document.Email, - Role = document.Role, + Role = Enum.Parse(document.Role, true), CreatedAt = document.CreatedAt, Permissions = document.Permissions ?? Enumerable.Empty(), IsEmailVerified = document.IsEmailVerified, diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.sln b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.sln new file mode 100644 index 000000000..3daed690a --- /dev/null +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/MiniSpace.Services.Identity.Infrastructure.UnitTests.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Identity.Infrastructure.UnitTests", "MiniSpace.Services.Identity.Infrastructure.UnitTests.csproj", "{A93FC3B5-DDF6-4C0C-8105-BA855AC29CCD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A93FC3B5-DDF6-4C0C-8105-BA855AC29CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A93FC3B5-DDF6-4C0C-8105-BA855AC29CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A93FC3B5-DDF6-4C0C-8105-BA855AC29CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A93FC3B5-DDF6-4C0C-8105-BA855AC29CCD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EEBD2815-FA20-40AB-BFE7-543D3A82FF85} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/MessageBrokerTests.cs b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/MessageBrokerTests.cs index d82a7b6cc..b30f600bf 100644 --- a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/MessageBrokerTests.cs +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/MessageBrokerTests.cs @@ -1,142 +1,142 @@ -using Xunit; -using Moq; -using Convey.CQRS.Events; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Convey.MessageBrokers; -using Convey.MessageBrokers.Outbox; -using Convey.MessageBrokers.RabbitMQ; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using OpenTracing; -using MiniSpace.Services.Identity.Application.Events; -using MiniSpace.Services.Identity.Application.Services; -using MiniSpace.Services.Identity.Infrastructure.Services; +// using Xunit; +// using Moq; +// using Convey.CQRS.Events; +// using System; +// using System.Collections.Generic; +// using System.Threading.Tasks; +// using Convey.MessageBrokers; +// using Convey.MessageBrokers.Outbox; +// using Convey.MessageBrokers.RabbitMQ; +// using Microsoft.AspNetCore.Http; +// using Microsoft.Extensions.Logging; +// using OpenTracing; +// using MiniSpace.Services.Identity.Application.Events; +// using MiniSpace.Services.Identity.Application.Services; +// using MiniSpace.Services.Identity.Infrastructure.Services; -namespace MiniSpace.Services.Identity.Infrastructure.UnitTests.Services -{ - public class MessageBrokerTests - { - private readonly MessageBroker _messageBroker; - private readonly Mock _mockBusPublisher; - private readonly Mock _mockMessageOutbox; - private readonly Mock _mockContextAccessor; - private readonly Mock _mockHttpContextAccessor; - private readonly Mock _mockMessagePropertiesAccessor; - private readonly Mock _mockTracer; - private readonly Mock> _mockLogger; +// namespace MiniSpace.Services.Identity.Infrastructure.UnitTests.Services +// { +// public class MessageBrokerTests +// { +// private readonly MessageBroker _messageBroker; +// private readonly Mock _mockBusPublisher; +// private readonly Mock _mockMessageOutbox; +// private readonly Mock _mockContextAccessor; +// private readonly Mock _mockHttpContextAccessor; +// private readonly Mock _mockMessagePropertiesAccessor; +// private readonly Mock _mockTracer; +// private readonly Mock> _mockLogger; - public MessageBrokerTests() - { - _mockBusPublisher = new Mock(); - _mockMessageOutbox = new Mock(); - _mockContextAccessor = new Mock(); - _mockHttpContextAccessor = new Mock(); - _mockMessagePropertiesAccessor = new Mock(); - _mockTracer = new Mock(); - _mockLogger = new Mock>(); +// public MessageBrokerTests() +// { +// _mockBusPublisher = new Mock(); +// _mockMessageOutbox = new Mock(); +// _mockContextAccessor = new Mock(); +// _mockHttpContextAccessor = new Mock(); +// _mockMessagePropertiesAccessor = new Mock(); +// _mockTracer = new Mock(); +// _mockLogger = new Mock>(); - _messageBroker = new MessageBroker(_mockBusPublisher.Object, _mockMessageOutbox.Object, _mockContextAccessor.Object, - _mockHttpContextAccessor.Object, _mockMessagePropertiesAccessor.Object, new RabbitMqOptions(), - _mockTracer.Object, _mockLogger.Object); - } +// _messageBroker = new MessageBroker(_mockBusPublisher.Object, _mockMessageOutbox.Object, _mockContextAccessor.Object, +// _mockHttpContextAccessor.Object, _mockMessagePropertiesAccessor.Object, new RabbitMqOptions(), +// _mockTracer.Object, _mockLogger.Object); +// } - [Fact] - public async Task PublishAsync_WithEventsAndOutboxDisabled_PublishesEvents() - { - //Arrange - var events = new List - { - new SignedIn(Guid.NewGuid(), "user") - }; - _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); +// [Fact] +// public async Task PublishAsync_WithEventsAndOutboxDisabled_PublishesEvents() +// { +// //Arrange +// var events = new List +// { +// new SignedIn(Guid.NewGuid(), "user") +// }; +// _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); - //Act - await _messageBroker.PublishAsync(events); +// //Act +// await _messageBroker.PublishAsync(events); - //Assert - _mockMessageOutbox.Verify( - x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>()), - Times.Never - ); - _mockBusPublisher.Verify( - x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>()), Times.Exactly(events.Count) - ); - } +// //Assert +// _mockMessageOutbox.Verify( +// x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny(), It.IsAny>()), +// Times.Never +// ); +// _mockBusPublisher.Verify( +// x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny>()), Times.Exactly(events.Count) +// ); +// } - [Fact] - public async Task PublishAsync_WithEventsAndOutboxEnabled_SendsMessagesToOutbox() - { - //Arrange - var events = new List - { - new SignedIn(Guid.NewGuid(), "user") - }; - _mockMessageOutbox.Setup(x => x.Enabled).Returns(true); +// [Fact] +// public async Task PublishAsync_WithEventsAndOutboxEnabled_SendsMessagesToOutbox() +// { +// //Arrange +// var events = new List +// { +// new SignedIn(Guid.NewGuid(), "user") +// }; +// _mockMessageOutbox.Setup(x => x.Enabled).Returns(true); - //Act - await _messageBroker.PublishAsync(events); +// //Act +// await _messageBroker.PublishAsync(events); - //Assert - _mockMessageOutbox.Verify( - x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>()), - Times.Exactly(events.Count) - ); - _mockBusPublisher.Verify( - x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>()), Times.Never - ); - } +// //Assert +// _mockMessageOutbox.Verify( +// x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny(), It.IsAny>()), +// Times.Exactly(events.Count) +// ); +// _mockBusPublisher.Verify( +// x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny>()), Times.Never +// ); +// } - [Fact] - public async Task PublishAsync_WithoutEvents_Returns() - { - //Arrange - List events = null; +// [Fact] +// public async Task PublishAsync_WithoutEvents_Returns() +// { +// //Arrange +// List events = null; - //Act - await _messageBroker.PublishAsync(events); +// //Act +// await _messageBroker.PublishAsync(events); - //Assert - _mockMessageOutbox.Verify( - x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>()), - Times.Never - ); - _mockBusPublisher.Verify( - x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>()), Times.Never - ); - } +// //Assert +// _mockMessageOutbox.Verify( +// x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny(), It.IsAny>()), +// Times.Never +// ); +// _mockBusPublisher.Verify( +// x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny>()), Times.Never +// ); +// } - [Fact] - public async Task PublishAsync_WithNullEventAndOutboxDisabled_PublishesOneLessEvent() - { - //Arrange - var events = new List - { - new SignedIn(Guid.NewGuid(), "user"), - null - }; - _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); +// [Fact] +// public async Task PublishAsync_WithNullEventAndOutboxDisabled_PublishesOneLessEvent() +// { +// //Arrange +// var events = new List +// { +// new SignedIn(Guid.NewGuid(), "user"), +// null +// }; +// _mockMessageOutbox.Setup(x => x.Enabled).Returns(false); - //Act - await _messageBroker.PublishAsync(events); +// //Act +// await _messageBroker.PublishAsync(events); - //Assert - _mockMessageOutbox.Verify( - x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny>()), - Times.Never - ); - _mockBusPublisher.Verify( - x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny>()), Times.Exactly(events.Count - 1) - ); - } - } -} +// //Assert +// _mockMessageOutbox.Verify( +// x => x.SendAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny(), It.IsAny>()), +// Times.Never +// ); +// _mockBusPublisher.Verify( +// x => x.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), +// It.IsAny(), It.IsAny>()), Times.Exactly(events.Count - 1) +// ); +// } +// } +// } diff --git a/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/TwoFactorCodeServiceTests.cs b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/TwoFactorCodeServiceTests.cs new file mode 100644 index 000000000..84d659291 --- /dev/null +++ b/MiniSpace.Services.Identity/tests/MiniSpace.Services.Identity.Infrastructure.UnitTests/Services/TwoFactorCodeServiceTests.cs @@ -0,0 +1,75 @@ +using System; +using Xunit; +using MiniSpace.Services.Identity.Application.Events; +using MiniSpace.Services.Identity.Application.Services; +using MiniSpace.Services.Identity.Infrastructure.Services; +using MiniSpace.Services.Identity.Infrastructure.Auth; + + +namespace MiniSpace.Services.Identity.Infrastructure.UnitTests.Services +{ + public class TwoFactorCodeServiceTests + { + private readonly TwoFactorCodeService _twoFactorCodeService; + + public TwoFactorCodeServiceTests() + { + _twoFactorCodeService = new TwoFactorCodeService(); + } + + [Fact] + public void ValidateCode_WithCorrectCode_ReturnsTrue() + { + // Arrange + string secret = "3WFKUZ3HGQVQCXWQZI7OUHXRNTLFT5RQ"; + string correctCode = _twoFactorCodeService.GenerateCode(secret); + + // Act + bool result = _twoFactorCodeService.ValidateCode(secret, correctCode); + + // Assert + Assert.True(result); + } + + [Fact] + public void ValidateCode_WithIncorrectCode_ReturnsFalse() + { + // Arrange + string secret = "3WFKUZ3HGQVQCXWQZI7OUHXRNTLFT5RQ"; + string incorrectCode = "000000"; // Invalid code + + // Act + bool result = _twoFactorCodeService.ValidateCode(secret, incorrectCode); + + // Assert + Assert.False(result); + } + + [Fact] + public void GenerateCode_GeneratesExpectedCode() + { + // Arrange + string secret = "3WFKUZ3HGQVQCXWQZI7OUHXRNTLFT5RQ"; + long currentUnixTime = GetUnixTimestamp(); + string expectedCode = ComputeTotp(secret, currentUnixTime); + + // Act + string generatedCode = _twoFactorCodeService.GenerateCode(secret); + + // Assert + Assert.Equal(expectedCode, generatedCode); + } + + private long GetUnixTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + private string ComputeTotp(string secret, long unixTime) + { + byte[] secretBytes = TwoFactorCodeService.Base32ToBytes(secret); + int otp = TwoFactorCodeService.ComputeTotp(secretBytes, unixTime / 30); + return otp.ToString("D6"); + } + } +} diff --git a/MiniSpace.Services.Identity/tests/api-p-test.py b/MiniSpace.Services.Identity/tests/api-p-test.py new file mode 100644 index 000000000..ef8bd02fc --- /dev/null +++ b/MiniSpace.Services.Identity/tests/api-p-test.py @@ -0,0 +1,29 @@ +from locust import HttpUser, TaskSet, task, between, events +import os + +class UserBehavior(TaskSet): + @task(1) + def sign_in(self): + self.client.post("/sign-in", json={"email": "austineccentric@gmail.com", "password": ""}) + +class WebsiteUser(HttpUser): + tasks = [UserBehavior] + wait_time = between(1, 2) + +if __name__ == "__main__": + # Default values + default_user_count = 50000 # 1% of total users + default_spawn_rate = default_user_count / 1800 # Spread the spawn over 30 minutes + + # Override with environment variables if available + user_count = int(os.getenv("LOCUST_USERS", default_user_count)) + spawn_rate = float(os.getenv("LOCUST_SPAWN_RATE", default_spawn_rate)) + + # Register environment variables + @events.init_command_line_parser.add_listener + def _(parser): + parser.add_argument("--users", type=int, default=user_count, help="Number of concurrent users") + parser.add_argument("--spawn-rate", type=float, default=spawn_rate, help="Rate to spawn users (users per second)") + + # Execute the test + os.system(f"locust -f api-p-test.py --host http://localhost:5004 --users {user_count} --spawn-rate {spawn_rate}") diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/.gitignore b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/.gitignore index d30eaf4d5..ed6d69a3a 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/.gitignore +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/.gitignore @@ -3,6 +3,12 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +appsettings.local.json +appsettings.docker.json +appsettings.json + + # User-specific files *.suo *.user diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs index b30e65e5b..0c0104671 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs @@ -18,6 +18,7 @@ using MiniSpace.Services.MediaFiles.Infrastructure; using DotNetEnv; using Convey.CQRS.Commands; +using System.Text; namespace MiniSpace.Services.MediaFiles.Api { @@ -35,6 +36,7 @@ await WebHost.CreateDefaultBuilder(args) .AddInfrastructure() .Build()) .Configure(app => app + // .UseMiddleware() .UseInfrastructure() .UseEndpoints(endpoints => endpoints .Post("media-files", async (cmd, ctx) => @@ -54,4 +56,35 @@ await WebHost.CreateDefaultBuilder(args) .RunAsync(); } } + + + + // public class RequestLoggingMiddleware + // { + // private readonly RequestDelegate _next; + + // public RequestLoggingMiddleware(RequestDelegate next) + // { + // _next = next; + // } + + // public async Task Invoke(HttpContext context) + // { + // // Enable buffering so we can read the request body multiple times + // context.Request.EnableBuffering(); + + // // Read the request body as a string + // var requestBody = await new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true).ReadToEndAsync(); + + // // Log the request body + // Console.WriteLine("Request Body:"); + // Console.WriteLine(requestBody); + + // // Reset the request body stream position so the next middleware can read it + // context.Request.Body.Position = 0; + + // // Continue processing the request + // await _next(context); + // } + // } } \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs index fed9b647c..1d722dd00 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Convey.CQRS.Commands; @@ -27,27 +28,45 @@ public DeleteMediaFileHandler(IFileSourceInfoRepository fileSourceInfoRepository public async Task HandleAsync(DeleteMediaFile command, CancellationToken cancellationToken) { + // Serialize the command to JSON and write to the console + var commandJson = JsonSerializer.Serialize(command, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine("Received DeleteMediaFile command: " + commandJson); + // Decode the URL before using it var decodedUrl = Uri.UnescapeDataString(command.MediaFileUrl); - Console.WriteLine($"DeleteMediaFileHandler: {decodedUrl}"); + + // Retrieve the file source information var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(decodedUrl); if (fileSourceInfo is null) { throw new MediaFileNotFoundException(decodedUrl); } + // Log the fileSourceInfo details to ensure it contains the correct data + var fileSourceInfoJson = JsonSerializer.Serialize(fileSourceInfo, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine("Retrieved FileSourceInfo: " + fileSourceInfoJson); + + // Check for unauthorized access var identity = _appContext.Identity; - if (identity.IsAuthenticated && identity.Id != fileSourceInfo.UploaderId) + if (identity.IsAuthenticated && identity.Id != fileSourceInfo.UploaderId && + !fileSourceInfo.OrganizationId.HasValue) { throw new UnauthorizedMediaFileAccessException(fileSourceInfo.Id, identity.Id, fileSourceInfo.UploaderId); } + // Delete the files from S3 await _s3Service.DeleteFileAsync(fileSourceInfo.OriginalFileUrl); await _s3Service.DeleteFileAsync(fileSourceInfo.FileUrl); await _fileSourceInfoRepository.DeleteAsync(decodedUrl); - await _messageBroker.PublishAsync(new MediaFileDeleted(decodedUrl, - fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString())); + + // Publish the MediaFileDeleted event + await _messageBroker.PublishAsync(new MediaFileDeleted( + decodedUrl, + fileSourceInfo.SourceId, + fileSourceInfo.SourceType.ToString(), + fileSourceInfo.UploaderId, + fileSourceInfo.OrganizationId)); } } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs index 459373162..5cb2e94cc 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs @@ -1,5 +1,5 @@ using Convey.CQRS.Commands; -using Microsoft.AspNetCore.Http; +using System; namespace MiniSpace.Services.MediaFiles.Application.Commands { @@ -8,21 +8,23 @@ public class UploadMediaFile : ICommand public Guid MediaFileId { get; set; } public Guid SourceId { get; set; } public string SourceType { get; set; } + public Guid? OrganizationId { get; set; } public Guid UploaderId { get; set; } public string FileName { get; set; } public string FileContentType { get; set; } - public string Base64Content { get; set; } + public byte[] FileData { get; set; } - public UploadMediaFile(Guid mediaFileId, Guid sourceId, string sourceType, Guid uploaderId, - string fileName, string fileContentType, string base64Content) + public UploadMediaFile(Guid mediaFileId, Guid sourceId, string sourceType, Guid? organizationId, + Guid uploaderId, string fileName, string fileContentType, byte[] fileData) { MediaFileId = mediaFileId == Guid.Empty ? Guid.NewGuid() : mediaFileId; SourceId = sourceId; SourceType = sourceType; + OrganizationId = organizationId; UploaderId = uploaderId; FileName = fileName; FileContentType = fileContentType; - Base64Content = base64Content; + FileData = fileData; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs index 45f30e82e..2a4b8f650 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs @@ -1,4 +1,5 @@ using Convey.CQRS.Events; +using System; namespace MiniSpace.Services.MediaFiles.Application.Events { @@ -7,12 +8,17 @@ public class MediaFileDeleted : IEvent public string MediaFileUrl { get; } public Guid SourceId { get; } public string Source { get; } + public Guid UploaderId { get; } + public Guid? OrganizationId { get; } - public MediaFileDeleted(string mediaFileUrl, Guid sourceId, string source) + public MediaFileDeleted(string mediaFileUrl, Guid sourceId, string source, + Guid uploaderId, Guid? organizationId) { MediaFileUrl = mediaFileUrl; SourceId = sourceId; Source = source; + UploaderId = uploaderId; + OrganizationId = organizationId; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/OrganizationImageUploaded.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/OrganizationImageUploaded.cs new file mode 100644 index 000000000..cd97af712 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/OrganizationImageUploaded.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class OrganizationImageUploaded : IEvent + { + public Guid OrganizationId { get; } + public string ImageUrl { get; } + public string ImageType { get; } + public DateTime UploadDate { get; } + + public OrganizationImageUploaded(Guid organizationId, string imageUrl, string imageType, DateTime uploadDate) + { + OrganizationId = organizationId; + ImageUrl = imageUrl; + ImageType = imageType; + UploadDate = uploadDate; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/StudentImageUploaded.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/StudentImageUploaded.cs index e01f3e7db..1af63e94a 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/StudentImageUploaded.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/StudentImageUploaded.cs @@ -1,19 +1,21 @@ using Convey.CQRS.Events; using System; -namespace MiniSpace.Services.MediaFiles.Application.Events.External +namespace MiniSpace.Services.MediaFiles.Application.Events { public class StudentImageUploaded : IEvent { public Guid StudentId { get; } public string ImageUrl { get; } public string ImageType { get; } + public DateTime UploadDate { get; } - public StudentImageUploaded(Guid studentId, string imageUrl, string imageType) + public StudentImageUploaded(Guid studentId, string imageUrl, string imageType, DateTime uploadDate) { StudentId = studentId; ImageUrl = imageUrl; ImageType = imageType; + UploadDate = uploadDate; } } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs index e570dec7c..d053a6317 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs @@ -3,10 +3,10 @@ public class InvalidFileSizeException : AppException { public override string Code { get; } = "invalid_file_size"; - public int FileSize { get; } - public int MaxFileSize { get; } + public long FileSize { get; } + public long MaxFileSize { get; } - public InvalidFileSizeException(int fileSize, int maxFileSize) + public InvalidFileSizeException(long fileSize, long maxFileSize) : base($"Invalid file size: {fileSize}. Maximum valid file size: {maxFileSize}.") { FileSize = fileSize; diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs index b6450def4..278f347a9 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs @@ -2,7 +2,7 @@ { public interface IFileValidator { - public void ValidateFileSize(int size); - public void ValidateFileExtensions(byte[] bytes, string contentType); + void ValidateFileSize(long size); + void ValidateFileExtensions(byte[] bytes, string contentType); } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs index 4fafe77bc..87e3478de 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs @@ -6,6 +6,9 @@ public enum ContextType Post, StudentProfileImage, StudentBannerImage, - StudentGalleryImage + StudentGalleryImage, + OrganizationProfileImage, + OrganizationBannerImage, + OrganizationGalleryImage } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs index 7ccd3a40f..8e16a6682 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs @@ -13,9 +13,11 @@ public class FileSourceInfo : AggregateRoot public string OriginalFileContentType { get; set; } public string FileUrl { get; set; } public string FileName { get; set; } - + public Guid? OrganizationId { get; set; } + public FileSourceInfo(Guid id, Guid sourceId, ContextType sourceType, Guid uploaderId, State state, - DateTime createdAt, string originalFileUrl, string originalFileContentType, string fileUrl, string fileName) + DateTime createdAt, string originalFileUrl, string originalFileContentType, string fileUrl, string fileName, + Guid? organizationId = null) { Id = id; SourceId = sourceId; @@ -27,13 +29,15 @@ public FileSourceInfo(Guid id, Guid sourceId, ContextType sourceType, Guid uploa OriginalFileContentType = originalFileContentType; FileUrl = fileUrl; FileName = fileName; + OrganizationId = organizationId; } - + + public void Associate() { State = State.Associated; } - + public void Unassociate() { State = State.Unassociated; diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs index 1b1a4eae3..5b9083d73 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs @@ -14,5 +14,6 @@ public interface IFileSourceInfoRepository Task> FindByUploaderIdAndSourceTypeAsync(Guid uploaderId, ContextType sourceType); Task> GetAllAsync(string url); Task DeleteAllAsync(string url); + Task> FindByOrganizationIdAsync(Guid organizationId); } } \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs index 9f3c159ce..60a6066fa 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs @@ -17,7 +17,8 @@ public static FileSourceInfo AsEntity(this FileSourceInfoDocument document) document.OriginalFileUrl, document.OriginalFileContentType, document.FileUrl, - document.FileName + document.FileName, + document.OrganizationId ); public static FileSourceInfoDocument AsDocument(this FileSourceInfo entity) @@ -32,7 +33,9 @@ public static FileSourceInfoDocument AsDocument(this FileSourceInfo entity) OriginalFileUrl = entity.OriginalFileUrl, OriginalFileContentType = entity.OriginalFileContentType, FileUrl = entity.FileUrl, - FileName = entity.FileName + FileName = entity.FileName, + OrganizationId = entity.OrganizationId }; + } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs index cbebbe56b..8bd018179 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs @@ -17,5 +17,6 @@ public class FileSourceInfoDocument : IIdentifiable public string OriginalFileContentType { get; set; } public string FileUrl { get; set; } public string FileName { get; set; } + public Guid? OrganizationId { get; set; } } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs index bc0514c69..c0b38a528 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs @@ -77,5 +77,12 @@ public async Task> FindByUploaderIdAndSourceTypeAsyn var fileSourceInfos = await _repository.FindAsync(s => s.UploaderId == uploaderId && s.SourceType == sourceType); return fileSourceInfos?.Select(s => s.AsEntity()); } + + // Optional: Implement method to find files by OrganizationId + public async Task> FindByOrganizationIdAsync(Guid organizationId) + { + var fileSourceInfos = await _repository.FindAsync(s => s.OrganizationId == organizationId); + return fileSourceInfos?.Select(s => s.AsEntity()); + } } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs index 9ca5c3fb6..00fcb3b60 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs @@ -1,11 +1,13 @@ -using MiniSpace.Services.MediaFiles.Application.Exceptions; +using System.Globalization; +using MiniSpace.Services.MediaFiles.Application.Exceptions; using MiniSpace.Services.MediaFiles.Application.Services; +using System.IO; namespace MiniSpace.Services.MediaFiles.Infrastructure.Services { public class FileValidator : IFileValidator { - private const int MaxFileSize = 5_000_000; + private const long MaxFileSize = 5_000_000; private readonly Dictionary _mimeTypes = new Dictionary() { @@ -14,15 +16,17 @@ public class FileValidator : IFileValidator { "FFD8FFE1", "image/jpeg" }, { "FFD8FFE2", "image/jpeg" }, { "FFD8FFEE", "image/jpeg" }, - { "89504E47", "image/png" }, - { "47494638", "image/gif" }, + { "89504E47", "image/png" }, + { "47494638", "image/gif" }, { "49492A00", "image/tiff" }, { "4D4D002A", "image/tiff" }, { "52494646", "image/webp" }, - { "57454250", "image/webp" } + { "57454250", "image/webp" }, + { "00000100", "image/ico" }, + { "00000200", "image/ico" } }; - public void ValidateFileSize(int size) + public void ValidateFileSize(long size) { if (size > MaxFileSize) { @@ -32,18 +36,37 @@ public void ValidateFileSize(int size) public void ValidateFileExtensions(byte[] bytes, string contentType) { - if (!_mimeTypes.ContainsValue(contentType)) + string hex = BitConverter.ToString(bytes, 0, 4).Replace("-", string.Empty).ToUpper(CultureInfo.InvariantCulture); + if (bytes.Length >= 8) { - throw new InvalidFileContentTypeException(contentType); + string extendedHex = BitConverter.ToString(bytes, 0, 8).Replace("-", string.Empty).ToUpper(CultureInfo.InvariantCulture); + if (_mimeTypes.TryGetValue(extendedHex, out var extendedMimeType)) + { + if (AreMimeTypesCompatible(extendedMimeType, contentType)) + { + return; // Matched with 8-byte signature + } + } + } + + if (_mimeTypes.TryGetValue(hex, out var mimeType)) + { + if (!AreMimeTypesCompatible(mimeType, contentType)) + { + throw new FileTypeDoesNotMatchContentTypeException(mimeType, contentType); + } } - - string hex = BitConverter.ToString(bytes, 0, 4).Replace("-", string.Empty); - _mimeTypes.TryGetValue(hex, out var mimeType); - if (mimeType != contentType) + else { - throw new FileTypeDoesNotMatchContentTypeException(mimeType, contentType); + throw new InvalidFileContentTypeException(contentType); } } - + + private bool AreMimeTypesCompatible(string detectedMimeType, string providedMimeType) + { + return string.Equals(detectedMimeType, providedMimeType, StringComparison.InvariantCultureIgnoreCase) || + (detectedMimeType == "image/jpeg" && providedMimeType == "image/jpg") || + (detectedMimeType == "image/jpg" && providedMimeType == "image/jpeg"); + } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs index 8a20a9fc1..bd77c5abc 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs @@ -9,10 +9,12 @@ using MiniSpace.Services.MediaFiles.Core.Repositories; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Processing; using System; using System.IO; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace MiniSpace.Services.MediaFiles.Infrastructure.Services @@ -40,6 +42,21 @@ public MediaFilesService(IFileSourceInfoRepository fileSourceInfoRepository, IFi public async Task UploadAsync(UploadMediaFile command) { + var commandWithoutFileData = new UploadMediaFile( + command.MediaFileId, + command.SourceId, + command.SourceType, + command.OrganizationId, + command.UploaderId, + command.FileName, + command.FileContentType, + null // Exclude the FileData + ); + + // Serialize the modified command to JSON and write to the console + var commandJson = JsonSerializer.Serialize(commandWithoutFileData, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine("Received UploadMediaFile command (excluding FileData): " + commandJson); + var identity = _appContext.Identity; if (identity.IsAuthenticated && identity.Id != command.UploaderId) { @@ -51,7 +68,11 @@ public async Task UploadAsync(UploadMediaFile command) throw new InvalidContextTypeException(command.SourceType); } - if (sourceType == ContextType.StudentProfileImage || sourceType == ContextType.StudentBannerImage) + // Handle previous files if necessary + if (sourceType == ContextType.StudentProfileImage || + sourceType == ContextType.StudentBannerImage || + sourceType == ContextType.OrganizationProfileImage || + sourceType == ContextType.OrganizationBannerImage) { var existingFiles = await _fileSourceInfoRepository.FindByUploaderIdAndSourceTypeAsync(command.UploaderId, sourceType); foreach (var existingFile in existingFiles) @@ -61,36 +82,61 @@ public async Task UploadAsync(UploadMediaFile command) } } - byte[] bytes = Convert.FromBase64String(command.Base64Content); - _fileValidator.ValidateFileSize(bytes.Length); - _fileValidator.ValidateFileExtensions(bytes, command.FileContentType); + _fileValidator.ValidateFileSize(command.FileData.Length); + + // Extract the first 8 bytes for validation + byte[] buffer = new byte[8]; + Array.Copy(command.FileData, 0, buffer, 0, Math.Min(buffer.Length, command.FileData.Length)); + _fileValidator.ValidateFileExtensions(buffer, command.FileContentType); - using var inStream = new MemoryStream(bytes); + // Load the image from the byte array + using var inStream = new MemoryStream(command.FileData); using var myImage = await Image.LoadAsync(inStream); + + // Process the image (e.g., resizing) using var outStream = new MemoryStream(); - await myImage.SaveAsync(outStream, new WebpEncoder { Quality = 75 }); - inStream.Position = 0; - outStream.Position = 0; + myImage.Mutate(x => x.Resize(new ResizeOptions + { + Mode = ResizeMode.Max, + Size = new Size(1024, 1024) // Adjust size for optimization + })); + await myImage.SaveAsync(outStream, new WebpEncoder { Quality = 50 }); + // Generate unique file names string originalFileName = GenerateUniqueFileName(command.SourceType, command.UploaderId, command.FileName); string webpFileName = GenerateUniqueFileName(command.SourceType, command.UploaderId, command.FileName, "webp"); - var originalUrl = await _s3Service.UploadFileAsync("images", originalFileName, inStream); - var processedUrl = await _s3Service.UploadFileAsync("webps", webpFileName, outStream); + // Upload original and processed files to S3 + var originalUrlTask = _s3Service.UploadFileAsync("images", originalFileName, inStream); + var processedUrlTask = _s3Service.UploadFileAsync("webps", webpFileName, outStream); + await Task.WhenAll(originalUrlTask, processedUrlTask); + + var originalUrl = await originalUrlTask; + var processedUrl = await processedUrlTask; + + // Store file info in the repository + var uploadDate = _dateTimeProvider.Now; var fileSourceInfo = new FileSourceInfo(command.MediaFileId, command.SourceId, sourceType, - command.UploaderId, State.Associated, _dateTimeProvider.Now, originalUrl, - command.FileContentType, processedUrl, originalFileName); + command.UploaderId, State.Associated, uploadDate, originalUrl, + command.FileContentType, processedUrl, originalFileName, command.OrganizationId); await _fileSourceInfoRepository.AddAsync(fileSourceInfo); await _messageBroker.PublishAsync(new MediaFileUploaded(command.MediaFileId, originalFileName)); - if (sourceType == ContextType.StudentProfileImage || - sourceType == ContextType.StudentBannerImage || - sourceType == ContextType.StudentGalleryImage) + // Handle specific events based on the source type and organization + if (command.OrganizationId.HasValue) { var imageType = sourceType.ToString(); - var studentImageUploadedEvent = new StudentImageUploaded(command.UploaderId, processedUrl, imageType); + var organizationImageUploadedEvent = new OrganizationImageUploaded(command.OrganizationId.Value, processedUrl, imageType, uploadDate); + await _messageBroker.PublishAsync(organizationImageUploadedEvent); + } + else if (sourceType == ContextType.StudentProfileImage || + sourceType == ContextType.StudentBannerImage || + sourceType == ContextType.StudentGalleryImage) + { + var imageType = sourceType.ToString(); + var studentImageUploadedEvent = new StudentImageUploaded(command.UploaderId, processedUrl, imageType, uploadDate); await _messageBroker.PublishAsync(studentImageUploadedEvent); } @@ -103,6 +149,11 @@ private string GenerateUniqueFileName(string contextType, Guid uploaderId, strin string hashedFileName = HashFileName(originalFileName); string fileExtension = extension ?? Path.GetExtension(originalFileName); + if (!fileExtension.StartsWith(".")) + { + fileExtension = "." + fileExtension; + } + return $"{contextType}_{uploaderId}_{timestamp}_{hashedFileName}{fileExtension}"; } @@ -112,6 +163,5 @@ private string HashFileName(string fileName) byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(fileName)); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } - } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/S3Service.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/S3Service.cs index 3a9725348..160a29aaf 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/S3Service.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/S3Service.cs @@ -21,7 +21,26 @@ public async Task UploadFileAsync(string folderName, string fileName, St { var fileTransferUtility = new TransferUtility(_s3Client); var key = $"{folderName}/{fileName}"; - await fileTransferUtility.UploadAsync(fileStream, BucketName, key); + + // If the file size is larger than a certain threshold, use multipart upload + if (fileStream.Length > 5 * 1024 * 1024) // 5 MB + { + var request = new TransferUtilityUploadRequest + { + InputStream = fileStream, + Key = key, + BucketName = BucketName, + StorageClass = S3StorageClass.Standard, + PartSize = 5 * 1024 * 1024, // 5 MB + ContentType = "image/webp" + }; + await fileTransferUtility.UploadAsync(request); + } + else + { + await fileTransferUtility.UploadAsync(fileStream, BucketName, key); + } + return $"https://{BucketName}.s3.amazonaws.com/{key}"; } diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore index 1f7e99963..94196c0d8 100644 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore @@ -3,6 +3,11 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +appsettings.local.json +appsettings.docker.json +appsettings.json + + # User-specific files *.suo *.user diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore copy b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore copy deleted file mode 100644 index 6f04bbaa1..000000000 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Api/.gitignore copy +++ /dev/null @@ -1,332 +0,0 @@ -## 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 -*.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 - -# 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.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs index e91c2e05d..43a11904f 100644 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Dto/StudentDto.cs @@ -9,16 +9,20 @@ public class StudentDto public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public int NumberOfFriends { get; set; } - public Guid ProfileImage { get; set; } + public string ProfileImageUrl { get; set; } public string Description { get; set; } - public DateTime DateOfBirth { 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 List InterestedInEvents { get; set; } - public List SignedUpEvents { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } } } diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EmailVerified.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EmailVerified.cs new file mode 100644 index 000000000..753772c2b --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/EmailVerified.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Message("identity")] + public class EmailVerified : IEvent + { + public Guid UserId { get; } + public string Email { get; } + public DateTime VerifiedAt { get; } + + public EmailVerified(Guid userId, string email, DateTime verifiedAt) + { + UserId = userId; + Email = email; + VerifiedAt = verifiedAt; + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EmailVerifiedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EmailVerifiedHandler.cs new file mode 100644 index 000000000..f9cdea30d --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/EmailVerifiedHandler.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Repositories; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class EmailVerifiedHandler : IEventHandler + { + private readonly IMessageBroker _messageBroker; + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + + public EmailVerifiedHandler( + IMessageBroker messageBroker, + IStudentNotificationsRepository studentNotificationsRepository) + { + _messageBroker = messageBroker; + _studentNotificationsRepository = studentNotificationsRepository; + } + + public async Task HandleAsync(EmailVerified @event, CancellationToken cancellationToken) + { + var userNotifications = await _studentNotificationsRepository.GetByStudentIdAsync(@event.UserId) + ?? new StudentNotifications(@event.UserId); + + var emailVerifiedMessage = $"Your email {@event.Email} has been successfully verified!"; + + var detailsHtml = $@" +

Dear User,
+ Congratulations! Your email address {@event.Email} has been successfully verified on { @event.VerifiedAt}.



+

Thank you for verifying your email address.

"; + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: @event.UserId, + message: emailVerifiedMessage, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: @event.UserId, + eventType: NotificationEventType.EmailVerified, + details: detailsHtml + ); + + userNotifications.AddNotification(notification); + await _studentNotificationsRepository.AddOrUpdateAsync(userNotifications); + + var notificationCreatedEvent = new NotificationCreated( + notificationId: Guid.NewGuid(), + userId: @event.UserId, + message: emailVerifiedMessage, + createdAt: DateTime.UtcNow, + eventType: NotificationEventType.EmailVerified.ToString(), + relatedEntityId: @event.UserId, + details: detailsHtml + ); + + await _messageBroker.PublishAsync(notificationCreatedEvent); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/TwoFactorCodeGeneratedHandler.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/TwoFactorCodeGeneratedHandler.cs new file mode 100644 index 000000000..cdae3c6c1 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/Handlers/TwoFactorCodeGeneratedHandler.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Notifications.Application.Services; +using MiniSpace.Services.Notifications.Core.Entities; +using MiniSpace.Services.Notifications.Core.Repositories; +using MiniSpace.Services.Notifications.Application.Services.Clients; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Notifications.Application.Events.External.Handlers +{ + public class TwoFactorCodeGeneratedHandler : IEventHandler + { + private readonly IMessageBroker _messageBroker; + private readonly IStudentNotificationsRepository _studentNotificationsRepository; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly ILogger _logger; + + public TwoFactorCodeGeneratedHandler( + IMessageBroker messageBroker, + IStudentNotificationsRepository studentNotificationsRepository, + IStudentsServiceClient studentsServiceClient, + ILogger logger) + { + _messageBroker = messageBroker; + _studentNotificationsRepository = studentNotificationsRepository; + _studentsServiceClient = studentsServiceClient; + _logger = logger; + } + + public async Task HandleAsync(TwoFactorCodeGenerated @event, CancellationToken cancellationToken) + { + var student = await _studentsServiceClient.GetAsync(@event.UserId); + if (student == null) + { + _logger.LogError($"Student with ID {@event.UserId} not found."); + return; + } + + var userNotifications = await _studentNotificationsRepository.GetByStudentIdAsync(@event.UserId) + ?? new StudentNotifications(@event.UserId); + + var notificationMessage = $"Your 2FA code is {@event.Code}. Please use this to complete your sign-in."; + + var notification = new Notification( + notificationId: Guid.NewGuid(), + userId: @event.UserId, + message: null, + status: NotificationStatus.Unread, + createdAt: DateTime.UtcNow, + updatedAt: null, + relatedEntityId: @event.UserId, + eventType: NotificationEventType.TwoFactorCodeGenerated, + details: notificationMessage + ); + + // userNotifications.AddNotification(notification); + // await _studentNotificationsRepository.AddOrUpdateAsync(userNotifications); + + var notificationCreatedEvent = new NotificationCreated( + notificationId: notification.NotificationId, + userId: @event.UserId, + message: notificationMessage, + createdAt: DateTime.UtcNow, + eventType: NotificationEventType.TwoFactorCodeGenerated.ToString(), + relatedEntityId: @event.UserId, + details: notificationMessage + ); + + await _messageBroker.PublishAsync(notificationCreatedEvent); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/TwoFactorCodeGenerated.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/TwoFactorCodeGenerated.cs new file mode 100644 index 000000000..9ed48cd77 --- /dev/null +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Application/Events/External/TwoFactorCodeGenerated.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Notifications.Application.Events.External +{ + [Message("identity")] + public class TwoFactorCodeGenerated : IEvent + { + public Guid UserId { get; } + public string Code { get; } + + public TwoFactorCodeGenerated(Guid userId, string code) + { + UserId = userId; + Code = code ?? throw new ArgumentNullException(nameof(code)); + } + } +} diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs index 6e866d7dc..51cae3f8e 100644 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Core/Entities/NotificationEventType.cs @@ -29,6 +29,8 @@ public enum NotificationEventType ReportResolved, ReportReviewStarted, ReportCancelled, - Other + Other, + EmailVerified, + TwoFactorCodeGenerated, } } \ No newline at end of file diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs index 65d2a2218..eb078bb71 100644 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Extensions.cs @@ -141,7 +141,9 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeEvent() .SubscribeEvent() .SubscribeEvent() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); return app; } diff --git a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs index 88f4d0b4d..39a3dff69 100644 --- a/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Notifications/src/MiniSpace.Services.Notifications.Infrastructure/Mongo/Documents/Extensions.cs @@ -150,13 +150,10 @@ public static StudentDto AsDto(this StudentDocument document) Email = document.Email, FirstName = document.FirstName, LastName = document.LastName, - NumberOfFriends = document.NumberOfFriends, - ProfileImage = document.ProfileImage, Description = document.Description, DateOfBirth = document.DateOfBirth, EmailNotifications = document.EmailNotifications, IsBanned = document.IsBanned, - IsOrganizer = document.IsOrganizer, State = document.State, CreatedAt = document.CreatedAt, InterestedInEvents = document.InterestedInEvents, diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/.gitignore b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs index 79a7e56b9..d73716b7a 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs @@ -34,19 +34,29 @@ public static async Task Main(string[] args) .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") .Get>("organizations/{organizationId}/children/all") - .Post("organizations", - afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/root")) - .Post("organizations/{organizationId}/children", + .Get>("users/{userId}/organizations") + .Get("organizations/{organizationId}/details/gallery-users") + .Get>("organizations/{organizationId}/roles") + + .Post("organizations", afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}")) + .Post("organizations/{organizationId}/children", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.SubOrganizationId}")) + .Post("organizations/{organizationId}/roles", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}/roles/{cmd.RoleName}")) .Delete("organizations/{organizationId}") - .Post("organizations/{organizationId}/organizer") - .Delete("organizations/{organizationId}/organizer/{organizerId}") .Post("organizations/{organizationId}/invite") + .Post("organizations/{organizationId}/roles/{memberId}") + .Put("organizations/{organizationId}/roles/{roleId}/permissions") .Post("organizations/{organizationId}/privacy") + .Put("organizations/{organizationId}/settings") + .Put("organizations/{organizationId}/visibility") + .Put("organizations/{organizationId}/feed") + .Put("organizations/{organizationId}", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}")) )) .UseLogging() .Build() 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 deleted file mode 100644 index 47077c04b..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AddOrganizerToOrganization.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Organizations.Application.Commands -{ - public class AddOrganizerToOrganization: ICommand - { - public Guid RootOrganizationId { get; set; } - public Guid OrganizationId { get; set; } - public Guid OrganizerId { get; set; } - - public AddOrganizerToOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) - { - RootOrganizationId = rootOrganizationId; - OrganizationId = organizationId; - OrganizerId = organizerId; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AssignRoleToMember.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AssignRoleToMember.cs new file mode 100644 index 000000000..17d03c1c5 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/AssignRoleToMember.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class AssignRoleToMember : ICommand + { + public Guid OrganizationId { get; } + public Guid MemberId { get; } + public string Role { get; } + + public AssignRoleToMember(Guid organizationId, Guid memberId, string role) + { + OrganizationId = organizationId; + MemberId = memberId; + Role = role; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs index 083448c53..e9b317196 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganization.cs @@ -1,21 +1,31 @@ using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Core.Entities; namespace MiniSpace.Services.Organizations.Application.Commands { - public class CreateOrganization: ICommand + public class CreateOrganization : ICommand { public Guid OrganizationId { get; } public string Name { get; } - public Guid RootId { get; } - public Guid ParentId { get; } + public string Description { get; } + public Guid? RootId { get; } + public Guid? ParentId { get; } + public Guid OwnerId { get; } + public OrganizationSettings Settings { get; } + public string BannerUrl { get; } + public string ImageUrl { get; } - public CreateOrganization(Guid organizationId, string name, Guid rootId, Guid parentId) + public CreateOrganization(Guid organizationId, string name, string description, Guid? rootId, Guid? parentId, Guid ownerId, OrganizationSettings settings, string bannerUrl, string imageUrl) { OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; Name = name; - RootId = rootId; - ParentId = parentId; + Description = description; + RootId = rootId; + ParentId = parentId; + OwnerId = ownerId; + Settings = settings; + BannerUrl = bannerUrl; + ImageUrl = imageUrl; } } } - diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganizationRole.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganizationRole.cs new file mode 100644 index 000000000..6aa925b7c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateOrganizationRole.cs @@ -0,0 +1,22 @@ +using Convey.CQRS.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class CreateOrganizationRole : ICommand + { + public Guid OrganizationId { get; } + public string RoleName { get; } + public string Description { get; } + public Dictionary Permissions { get; } + + public CreateOrganizationRole(Guid organizationId, string roleName, string description, Dictionary permissions) + { + OrganizationId = organizationId; + RoleName = roleName; + Description = description; + Permissions = permissions; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs deleted file mode 100644 index a8e630776..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateRootOrganization.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Organizations.Application.Commands -{ - public class CreateRootOrganization: ICommand - { - public Guid OrganizationId { get; } - public string Name { get; } - - public CreateRootOrganization(Guid organizationId, string name) - { - OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; - Name = name; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateSubOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateSubOrganization.cs new file mode 100644 index 000000000..cbe1555d2 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/CreateSubOrganization.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class CreateSubOrganization : ICommand + { + public Guid SubOrganizationId { get; } + public string Name { get; } + public string Description { get; } + public Guid RootId { get; } + public Guid ParentId { get; } + public Guid OwnerId { get; } + public OrganizationSettings Settings { get; } + public string BannerUrl { get; } + public string ImageUrl { get; } + + public CreateSubOrganization(Guid subOrganizationId, string name, string description, Guid rootId, Guid parentId, Guid ownerId, OrganizationSettings settings, string bannerUrl, string imageUrl) + { + SubOrganizationId = subOrganizationId == Guid.Empty ? Guid.NewGuid() : subOrganizationId; + Name = name; + Description = description; + RootId = rootId; + ParentId = parentId; + OwnerId = ownerId; + Settings = settings; + BannerUrl = bannerUrl; + ImageUrl = imageUrl; + } + } +} 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 deleted file mode 100644 index d5e3e59cb..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AddOrganizerToOrganizationHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 root = await _organizationRepository.GetAsync(command.RootOrganizationId); - if (root is null) - { - throw new RootOrganizationNotFoundException(command.RootOrganizationId); - } - - var organization = root.GetSubOrganization(command.OrganizationId); - if (organization == 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(root); - 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/AssignRoleToMemberHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AssignRoleToMemberHandler.cs new file mode 100644 index 000000000..d9c12f49e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/AssignRoleToMemberHandler.cs @@ -0,0 +1,69 @@ +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.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class AssignRoleToMemberHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + private readonly IOrganizationMembersRepository _organizationMembersRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public AssignRoleToMemberHandler( + IOrganizationRepository organizationRepository, + IOrganizationRolesRepository organizationRolesRepository, + IOrganizationMembersRepository organizationMembersRepository, + IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizationRolesRepository = organizationRolesRepository; + _organizationMembersRepository = organizationMembersRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AssignRoleToMember command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var member = await _organizationMembersRepository.GetMemberAsync(command.OrganizationId, command.MemberId); + if (member == null) + { + throw new MemberNotFoundException(command.MemberId); + } + + // Fetch the role by its name using the correct method signature + var existingRole = await _organizationRolesRepository.GetRoleByNameAsync(command.OrganizationId, command.Role); + if (existingRole == null) + { + throw new RoleNotFoundException(command.Role); + } + + // Assign the role to the member + organization.AssignRole(command.MemberId, existingRole.Name); + await _organizationRepository.UpdateAsync(organization); + + await _messageBroker.PublishAsync(new RoleAssignedToMember(organization.Id, command.MemberId, existingRole.Name, DateTime.UtcNow)); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs index 056f13b66..a2a8a44bd 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationHandler.cs @@ -4,19 +4,34 @@ using MiniSpace.Services.Organizations.Application.Services; using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MiniSpace.Services.Organizations.Application.Commands.Handlers { public class CreateOrganizationHandler : ICommandHandler { private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationMembersRepository _organizationMembersRepository; + private readonly IOrganizationGalleryRepository _organizationGalleryRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; private readonly IAppContext _appContext; private readonly IMessageBroker _messageBroker; - public CreateOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext, + public CreateOrganizationHandler( + IOrganizationRepository organizationRepository, + IOrganizationMembersRepository organizationMembersRepository, + IOrganizationGalleryRepository organizationGalleryRepository, + IOrganizationRolesRepository organizationRolesRepository, + IAppContext appContext, IMessageBroker messageBroker) { _organizationRepository = organizationRepository; + _organizationMembersRepository = organizationMembersRepository; + _organizationGalleryRepository = organizationGalleryRepository; + _organizationRolesRepository = organizationRolesRepository; _appContext = appContext; _messageBroker = messageBroker; } @@ -24,32 +39,94 @@ public CreateOrganizationHandler(IOrganizationRepository organizationRepository, public async Task HandleAsync(CreateOrganization command, CancellationToken cancellationToken) { var identity = _appContext.Identity; - if(identity.IsAuthenticated && !identity.IsAdmin) + if (!identity.IsAuthenticated) { - throw new Exceptions.UnauthorizedAccessException("admin"); + throw new UnauthorizedAccessException("User is not authenticated."); + } + + Organization organization; + + if (command.ParentId == null) + { + // Create as a root organization + organization = new Organization( + command.OrganizationId, + command.Name, + command.Description, + command.Settings, + command.OwnerId, + command.BannerUrl, + command.ImageUrl, + null // No parent organization + ); + + await _organizationRepository.AddAsync(organization); + } + else + { + // Handle creation of a sub-organization + var root = await _organizationRepository.GetAsync(command.RootId.Value); + if (root == null) + { + throw new RootOrganizationNotFoundException(command.RootId.Value); + } + + var parent = root.GetSubOrganization(command.ParentId.Value); + if (parent == null) + { + throw new ParentOrganizationNotFoundException(command.ParentId.Value); + } + + organization = new Organization( + command.OrganizationId, + command.Name, + command.Description, + command.Settings, + command.OwnerId, + command.BannerUrl, + command.ImageUrl, + command.ParentId.Value + ); + + parent.AddSubOrganization(organization); + await _organizationRepository.UpdateAsync(root); } - var root = await _organizationRepository.GetAsync(command.RootId); - if(root is null) + var defaultRoles = organization.Roles.ToList(); + foreach (var role in defaultRoles) { - throw new RootOrganizationNotFoundException(command.RootId); + await _organizationRolesRepository.AddRoleAsync(organization.Id, role); } - var parent = root.GetSubOrganization(command.ParentId); - if(parent is null) + // Initialize an empty gallery for the organization + await _organizationGalleryRepository.AddImageAsync(organization.Id, new GalleryImage(Guid.NewGuid(), "Default Image URL", DateTime.UtcNow)); + + // Add the creator as a member with the "Creator" role + var creatorRole = defaultRoles.SingleOrDefault(r => r.Name == "Creator"); + if (creatorRole == null) { - throw new ParentOrganizationNotFoundException(command.ParentId); + throw new RoleNotFoundException("Creator"); } - - if (string.IsNullOrWhiteSpace(command.Name)) + + var creatorMember = new User(identity.Id, creatorRole); + await _organizationMembersRepository.AddMemberAsync(organization.Id, creatorMember); + + var userRole = defaultRoles.SingleOrDefault(r => r.Name == "User"); + if (userRole == null) { - throw new InvalidOrganizationNameException(command.Name); + throw new RoleNotFoundException("User"); } - - var organization = new Organization(command.OrganizationId, command.Name); - parent.AddSubOrganization(organization); - await _organizationRepository.UpdateAsync(root); - await _messageBroker.PublishAsync(new OrganizationCreated(organization.Id, organization.Name, parent.Id)); + organization.UpdateDefaultRole(userRole.Name); + + + await _messageBroker.PublishAsync(new OrganizationCreated( + organization.Id, + organization.Name, + organization.Description, + command.RootId ?? organization.Id, + command.ParentId, + command.OwnerId, + DateTime.UtcNow)); } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationRoleHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationRoleHandler.cs new file mode 100644 index 000000000..a3c73fdfb --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateOrganizationRoleHandler.cs @@ -0,0 +1,81 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Application.Exceptions; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class CreateOrganizationRoleHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + private readonly IAppContext _appContext; + + public CreateOrganizationRoleHandler(IOrganizationRepository organizationRepository, IOrganizationRolesRepository organizationRolesRepository, IAppContext appContext) + { + _organizationRepository = organizationRepository; + _organizationRolesRepository = organizationRolesRepository; + _appContext = appContext; + } + + public async Task HandleAsync(CreateOrganizationRole command, CancellationToken cancellationToken) + { + // Log the entire command to the console + Console.WriteLine($"Handling CreateOrganizationRole Command:\n" + + $"OrganizationId: {command.OrganizationId}\n" + + $"RoleName: {command.RoleName}\n" + + $"Description: {command.Description}\n" + + $"Permissions: {string.Join(", ", command.Permissions.Select(p => $"{p.Key}: {p.Value}"))}"); + + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var user = await _organizationRepository.GetMemberAsync(command.OrganizationId, identity.Id); + if (user == null) + { + throw new UnauthorizedAccessException("User is not a member of the organization."); + } + + var role = await _organizationRolesRepository.GetRoleByNameAsync(organization.Id, user.Role.Name); + + if (role == null || !(role.Permissions.ContainsKey(Permission.EditPermissions) && role.Permissions[Permission.EditPermissions]) + && !(role.Permissions.ContainsKey(Permission.AssignRoles) && role.Permissions[Permission.AssignRoles])) + { + throw new UnauthorizedAccessException("User does not have permission to create roles."); + } + + var permissions = new Dictionary(); + foreach (var permission in command.Permissions) + { + if (Enum.TryParse(permission.Key, true, out var parsedPermission)) // Case-insensitive parsing + { + permissions[parsedPermission] = permission.Value; + } + else + { + throw new InvalidPermissionException(permission.Key); + } + } + + var newRole = new Role(command.RoleName, command.Description, permissions); + organization.AddRole(newRole); + + await _organizationRolesRepository.AddRoleAsync(command.OrganizationId, newRole); + await _organizationRepository.UpdateAsync(organization); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs deleted file mode 100644 index 1d21c515b..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateRootOrganizationHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -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.Entities; -using MiniSpace.Services.Organizations.Core.Repositories; - -namespace MiniSpace.Services.Organizations.Application.Commands.Handlers -{ - public class CreateRootOrganizationHandler : ICommandHandler - { - private readonly IOrganizationRepository _organizationRepository; - private readonly IAppContext _appContext; - private readonly IMessageBroker _messageBroker; - - public CreateRootOrganizationHandler(IOrganizationRepository organizationRepository, IAppContext appContext, - IMessageBroker messageBroker) - { - _organizationRepository = organizationRepository; - _appContext = appContext; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(CreateRootOrganization command, CancellationToken cancellationToken) - { - var identity = _appContext.Identity; - if(identity.IsAuthenticated && !identity.IsAdmin) - { - throw new Exceptions.UnauthorizedAccessException("admin"); - } - - if (string.IsNullOrWhiteSpace(command.Name)) - { - throw new InvalidOrganizationNameException(command.Name); - } - - var organization = new Organization(command.OrganizationId, command.Name); - await _organizationRepository.AddAsync(organization); - await _messageBroker.PublishAsync(new RootOrganizationCreated(organization.Id, organization.Name)); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateSubOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateSubOrganizationHandler.cs new file mode 100644 index 000000000..f89248c77 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/CreateSubOrganizationHandler.cs @@ -0,0 +1,91 @@ +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.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class CreateSubOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public CreateSubOrganizationHandler( + IOrganizationRepository organizationRepository, + IOrganizationRolesRepository organizationRolesRepository, + IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizationRolesRepository = organizationRolesRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CreateSubOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var root = await _organizationRepository.GetAsync(command.RootId); + if (root == null) + { + throw new RootOrganizationNotFoundException(command.RootId); + } + + var parent = root.GetSubOrganization(command.ParentId); + if (parent == null) + { + throw new ParentOrganizationNotFoundException(command.ParentId); + } + + var user = await _organizationRepository.GetMemberAsync(root.Id, identity.Id); + if (user == null) + { + throw new UnauthorizedAccessException("User is not a member of the organization."); + } + + var role = await _organizationRolesRepository.GetRoleByNameAsync(root.Id, user.Role.Name); + + if (role == null || !(role.Permissions.ContainsKey(Permission.CreateSubGroups) && role.Permissions[Permission.CreateSubGroups])) + { + throw new UnauthorizedAccessException("User does not have permission to create sub-organizations."); + } + + if (string.IsNullOrWhiteSpace(command.Name)) + { + throw new InvalidOrganizationNameException(command.Name); + } + + var organization = new Organization( + command.SubOrganizationId, + command.Name, + command.Description, + command.Settings, + command.OwnerId, + command.BannerUrl, + command.ImageUrl); + + parent.AddSubOrganization(organization); + await _organizationRepository.UpdateAsync(root); + await _messageBroker.PublishAsync(new OrganizationCreated( + organization.Id, + organization.Name, + organization.Description, + command.RootId, + command.ParentId, + command.OwnerId, + DateTime.UtcNow)); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs index bc9530d0c..be04e33fd 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/DeleteOrganizationHandler.cs @@ -22,9 +22,9 @@ public DeleteOrganizationHandler(IOrganizationRepository organizationRepository, public async Task HandleAsync(DeleteOrganization command, CancellationToken cancellationToken) { var identity = _appContext.Identity; - if(identity.IsAuthenticated && !identity.IsAdmin) + if(!identity.IsAuthenticated) { - throw new Exceptions.UnauthorizedAccessException("admin"); + throw new UserUnauthorizedAccessException("user"); } var root = await _organizationRepository.GetAsync(command.RootId); diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/InviteUserToOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/InviteUserToOrganizationHandler.cs new file mode 100644 index 000000000..803666a3b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/InviteUserToOrganizationHandler.cs @@ -0,0 +1,60 @@ +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.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class InviteUserToOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationMembersRepository _organizationMembersRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public InviteUserToOrganizationHandler( + IOrganizationRepository organizationRepository, + IOrganizationMembersRepository organizationMembersRepository, + IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizationMembersRepository = organizationMembersRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(InviteUserToOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var user = await _organizationMembersRepository.GetMemberAsync(organization.Id, identity.Id); + if (user == null || !user.HasPermission(Permission.InviteUsers)) + { + throw new UnauthorizedAccessException("User does not have permission to invite users."); + } + + organization.InviteUser(command.UserId); + await _organizationRepository.UpdateAsync(organization); + await _messageBroker.PublishAsync(new UserInvitedToOrganization( + command.OrganizationId, + command.UserId, + DateTime.UtcNow)); + } + } +} 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 deleted file mode 100644 index d20145909..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/RemoveOrganizerFromOrganizationHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -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 root = await _organizationRepository.GetAsync(command.RootOrganizationId); - if (root is null) - { - throw new RootOrganizationNotFoundException(command.RootOrganizationId); - } - - var organization = root.GetSubOrganization(command.OrganizationId); - if (organization == 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(root); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationHandler.cs new file mode 100644 index 000000000..77dc02d2c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationHandler.cs @@ -0,0 +1,101 @@ +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.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class UpdateOrganizationHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public UpdateOrganizationHandler( + IOrganizationRepository organizationRepository, + IOrganizationRolesRepository organizationRolesRepository, + IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizationRolesRepository = organizationRolesRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateOrganization command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var existingOrganization = await _organizationRepository.GetAsync(command.OrganizationId); + if (existingOrganization == null) + { + var root = await _organizationRepository.GetAsync(command.RootId); + if (root == null) + { + throw new RootOrganizationNotFoundException(command.RootId); + } + + var parent = root.GetSubOrganization(command.ParentId); + if (parent == null) + { + throw new ParentOrganizationNotFoundException(command.ParentId); + } + + if (string.IsNullOrWhiteSpace(command.Name)) + { + throw new InvalidOrganizationNameException(command.Name); + } + + var newOrganization = new Organization( + command.OrganizationId, + command.Name, + command.Description, + command.Settings, + command.OwnerId, + command.BannerUrl, + command.ImageUrl, + command.ParentId, + null, + command.DefaultRoleName + ); + + parent.AddSubOrganization(newOrganization); + await _organizationRepository.UpdateAsync(root); + } + else + { + var user = await _organizationRepository.GetMemberAsync(existingOrganization.Id, identity.Id); + if (user == null) + { + throw new UnauthorizedAccessException("User does not have permission to update the organization."); + } + + var role = await _organizationRolesRepository.GetRoleByNameAsync(existingOrganization.Id, user.Role.Name); + if (role == null || !role.Permissions.ContainsKey(Permission.EditOrganizationDetails) || !role.Permissions[Permission.EditOrganizationDetails]) + { + throw new UnauthorizedAccessException("User does not have permission to update the organization."); + } + + existingOrganization.UpdateDetails(command.Name, command.Description, command.Settings, command.BannerUrl, command.ImageUrl); + existingOrganization.UpdateDefaultRole(command.DefaultRoleName); + await _organizationRepository.UpdateAsync(existingOrganization); + } + + await _messageBroker.PublishAsync(new OrganizationUpserted( + command.OrganizationId, + existingOrganization != null, + DateTime.UtcNow)); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationSettingsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationSettingsHandler.cs new file mode 100644 index 000000000..33414214f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateOrganizationSettingsHandler.cs @@ -0,0 +1,67 @@ +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.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class UpdateOrganizationSettingsHandler : ICommandHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public UpdateOrganizationSettingsHandler( + IOrganizationRepository organizationRepository, + IOrganizationRolesRepository organizationRolesRepository, + IAppContext appContext, + IMessageBroker messageBroker) + { + _organizationRepository = organizationRepository; + _organizationRolesRepository = organizationRolesRepository; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateOrganizationSettings command, CancellationToken cancellationToken) + { + var identity = _appContext.Identity; + if (!identity.IsAuthenticated) + { + throw new UnauthorizedAccessException("User is not authenticated."); + } + + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var user = await _organizationRepository.GetMemberAsync(organization.Id, identity.Id); + if (user == null) + { + throw new UnauthorizedAccessException("User does not have permission to update organization settings."); + } + + var role = await _organizationRolesRepository.GetRoleByNameAsync(organization.Id, user.Role.Name); + + if (role == null || !role.Permissions.ContainsKey(Permission.EditOrganizationDetails) || !role.Permissions[Permission.EditOrganizationDetails]) + { + throw new UnauthorizedAccessException("User does not have permission to update organization settings."); + } + + organization.UpdateSettings(command.Settings); + await _organizationRepository.UpdateAsync(organization); + await _messageBroker.PublishAsync(new OrganizationSettingsUpdated( + organization.Id, + command.Settings, + DateTime.UtcNow)); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateRolePermissionsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateRolePermissionsHandler.cs new file mode 100644 index 000000000..e653b2943 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/UpdateRolePermissionsHandler.cs @@ -0,0 +1,59 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Organizations.Application.Exceptions; +using System.Threading; +using System.Collections.Generic; + +namespace MiniSpace.Services.Organizations.Application.Commands.Handlers +{ + public class UpdateRolePermissionsHandler : ICommandHandler + { + private readonly IOrganizationRolesRepository _rolesRepository; + private readonly IOrganizationRepository _organizationRepository; + + public UpdateRolePermissionsHandler(IOrganizationRolesRepository rolesRepository, IOrganizationRepository organizationRepository) + { + _rolesRepository = rolesRepository; + _organizationRepository = organizationRepository; + } + + public async Task HandleAsync(UpdateRolePermissions command, CancellationToken cancellationToken) + { + var organization = await _organizationRepository.GetAsync(command.OrganizationId); + if (organization == null) + { + throw new OrganizationNotFoundException(command.OrganizationId); + } + + var role = await _rolesRepository.GetRoleByNameAsync(command.OrganizationId, command.RoleName); + if (role == null) + { + throw new RoleNotFoundException(command.RoleName); + } + + role.UpdateName(command.RoleName); + role.UpdateDescription(command.Description); + + var permissions = new Dictionary(); + foreach (var permission in command.Permissions) + { + // Convert the string key to match enum case sensitivity + if (Enum.TryParse(permission.Key, true, out var parsedPermission)) + { + permissions[parsedPermission] = permission.Value; + } + else + { + throw new InvalidPermissionException(permission.Key); + } + } + role.UpdatePermissions(permissions); + + await _rolesRepository.UpdateRoleAsync(role); + await _organizationRepository.UpdateAsync(organization); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/InviteUserToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/InviteUserToOrganization.cs new file mode 100644 index 000000000..4cf66956f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/InviteUserToOrganization.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class InviteUserToOrganization : ICommand + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + + public InviteUserToOrganization(Guid organizationId, Guid userId) + { + OrganizationId = organizationId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/ManageFeed.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/ManageFeed.cs new file mode 100644 index 000000000..59374c613 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/ManageFeed.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class ManageFeed : ICommand + { + public Guid OrganizationId { get; } + public string Content { get; } + public string Action { get; } + + public ManageFeed(Guid organizationId, string content, string action) + { + OrganizationId = organizationId; + Content = content; + Action = action; + } + } +} 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 deleted file mode 100644 index 100ada5dd..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/RemoveOrganizerFromOrganization.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Organizations.Application.Commands -{ - public class RemoveOrganizerFromOrganization : ICommand - { - public Guid RootOrganizationId { get; set; } - public Guid OrganizationId { get; set; } - public Guid OrganizerId { get; set; } - - public RemoveOrganizerFromOrganization(Guid rootOrganizationId, Guid organizationId, Guid organizerId) - { - RootOrganizationId = rootOrganizationId; - OrganizationId = organizationId; - OrganizerId = organizerId; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationPrivacy.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationPrivacy.cs new file mode 100644 index 000000000..569342799 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationPrivacy.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class SetOrganizationPrivacy : ICommand + { + public Guid OrganizationId { get; } + public bool IsPrivate { get; } + + public SetOrganizationPrivacy(Guid organizationId, bool isPrivate) + { + OrganizationId = organizationId; + IsPrivate = isPrivate; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationVisibility.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationVisibility.cs new file mode 100644 index 000000000..008e6816c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/SetOrganizationVisibility.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class SetOrganizationVisibility : ICommand + { + public Guid OrganizationId { get; } + public bool IsVisible { get; } + + public SetOrganizationVisibility(Guid organizationId, bool isVisible) + { + OrganizationId = organizationId; + IsVisible = isVisible; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganization.cs new file mode 100644 index 000000000..5ae163bda --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganization.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class UpdateOrganization : ICommand + { + public Guid OrganizationId { get; private set; } + public string Name { get; } + public string Description { get; } + public Guid RootId { get; } + public Guid ParentId { get; } + public Guid OwnerId { get; } + public OrganizationSettings Settings { get; } + public string BannerUrl { get; } + public string ImageUrl { get; } + public string DefaultRoleName { get; } + + public UpdateOrganization(Guid organizationId, string name, string description, + Guid rootId, Guid parentId, Guid ownerId, OrganizationSettings settings, + string bannerUrl, string imageUrl, string defaultRoleName) + { + OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; + Name = name; + Description = description; + RootId = rootId; + ParentId = parentId; + OwnerId = ownerId; + Settings = settings; + BannerUrl = bannerUrl; + ImageUrl = imageUrl; + DefaultRoleName = defaultRoleName; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganizationSettings.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganizationSettings.cs new file mode 100644 index 000000000..4578bdbdf --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateOrganizationSettings.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class UpdateOrganizationSettings : ICommand + { + public Guid OrganizationId { get; } + public OrganizationSettings Settings { get; } + + public UpdateOrganizationSettings(Guid organizationId, OrganizationSettings settings) + { + OrganizationId = organizationId; + Settings = settings; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateRolePermissions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateRolePermissions.cs new file mode 100644 index 000000000..235817895 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/UpdateRolePermissions.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Organizations.Application.Commands +{ + public class UpdateRolePermissions : ICommand + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string RoleName { get; set; } + public string Description { get; set; } + public Dictionary Permissions { get; } + + public UpdateRolePermissions(Guid organizationId, Guid roleId, string roleName, + string description, Dictionary permissions) + { + OrganizationId = organizationId; + RoleId = roleId; + RoleName = roleName; + Description = description; + Permissions = permissions; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/GalleryImageDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/GalleryImageDto.cs new file mode 100644 index 000000000..0e33acd81 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/GalleryImageDto.cs @@ -0,0 +1,33 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class GalleryImageDto + { + public Guid ImageId { get; set; } + public string ImageUrl { get; set; } + public DateTime DateAdded { get; set; } + + public GalleryImageDto() + { + + } + + public GalleryImageDto(GalleryImage galleryImage) + { + ImageId = galleryImage.ImageId; + ImageUrl = galleryImage.ImageUrl; + DateAdded = galleryImage.DateAdded; + } + + public GalleryImageDto(Guid imageId, string imageUrl, DateTime dateAdded) + { + ImageId = imageId; + ImageUrl = imageUrl; + DateAdded = dateAdded; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/InvitationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/InvitationDto.cs new file mode 100644 index 000000000..20a3a365a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/InvitationDto.cs @@ -0,0 +1,22 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class InvitationDto + { + public Guid UserId { get; set; } + + public InvitationDto() + { + + } + + public InvitationDto(Invitation invitation) + { + UserId = invitation.UserId; + } + } +} 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 index 9c6d89049..ce55f44d0 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDetailsDto.cs @@ -1,4 +1,6 @@ -using System.Collections; +using System; +using System.Collections.Generic; +using System.Linq; using MiniSpace.Services.Organizations.Core.Entities; using System.Diagnostics.CodeAnalysis; @@ -9,20 +11,39 @@ public class OrganizationDetailsDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid RootId { get; set; } - public IEnumerable Organizers { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + public Guid? ParentOrganizationId { get; set; } + public IEnumerable SubOrganizations { get; set; } + public IEnumerable Invitations { get; set; } + public IEnumerable Users { get; set; } + public IEnumerable Roles { get; set; } + public IEnumerable Gallery { get; set; } + public OrganizationSettingsDto Settings { get; set; } + public string DefaultRoleName { get; set; } public OrganizationDetailsDto() { - } - public OrganizationDetailsDto(Organization organization, Guid rootId) + public OrganizationDetailsDto(Organization organization) { Id = organization.Id; Name = organization.Name; - RootId = rootId; - Organizers = organization.Organizers.Select(o => o.Id); + Description = organization.Description; + BannerUrl = organization.BannerUrl; + ImageUrl = organization.ImageUrl; + OwnerId = organization.OwnerId; + ParentOrganizationId = organization.ParentOrganizationId; + SubOrganizations = organization.SubOrganizations?.Select(o => new SubOrganizationDto(o)).ToList(); + Invitations = organization.Invitations?.Select(i => new InvitationDto(i)).ToList(); + Users = organization.Users?.Select(u => new UserDto(u)).ToList(); + Roles = organization.Roles?.Select(r => new RoleDto(r)).ToList(); + Gallery = organization.Gallery?.Select(g => new GalleryImageDto(g)).ToList(); + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null; + DefaultRoleName = organization.DefaultRoleName; } } -} \ 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 index 0aea03d2f..108ff88cf 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs @@ -8,19 +8,26 @@ public class OrganizationDto { public Guid Id { get; set; } public string Name { get; set; } - public Guid RootId { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + public string DefaultRoleName { get; set; } public OrganizationDto() { } - - public OrganizationDto (Organization organization, Guid rootId) + + public OrganizationDto(Organization organization) { Id = organization.Id; Name = organization.Name; - RootId = rootId; + Description = organization.Description; + BannerUrl = organization.BannerUrl; + ImageUrl = organization.ImageUrl; + OwnerId = organization.OwnerId; + DefaultRoleName = organization.DefaultRoleName; } } } - diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryDto.cs new file mode 100644 index 000000000..ca7e7da14 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class OrganizationGalleryDto + { + public OrganizationDto Organization { get; set; } + public IEnumerable Gallery { get; set; } + + public OrganizationGalleryDto(Organization organization, IEnumerable gallery) + { + Organization = new OrganizationDto(organization); + Gallery = gallery.Select(g => new GalleryImageDto(g)).ToList(); + } + } + +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryUsersDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryUsersDto.cs new file mode 100644 index 000000000..5ed5dbf36 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationGalleryUsersDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class OrganizationGalleryUsersDto + { + public OrganizationDetailsDto OrganizationDetails { get; set; } + public IEnumerable Gallery { get; set; } + public IEnumerable Users { get; set; } + public OrganizationGalleryUsersDto() {} + public OrganizationGalleryUsersDto(Organization organization, IEnumerable gallery, IEnumerable users) + { + OrganizationDetails = new OrganizationDetailsDto(organization); + Gallery = gallery.Select(g => new GalleryImageDto(g)).ToList(); + Users = users.Select(u => new UserDto(u)).ToList(); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationSettingsDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationSettingsDto.cs new file mode 100644 index 000000000..ea28ce251 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationSettingsDto.cs @@ -0,0 +1,45 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class OrganizationSettingsDto + { + public bool IsVisible { get; set; } + public bool IsPublic { get; set; } + public bool IsPrivate { get; set; } + public bool CanAddComments { get; set; } + public bool CanAddReactions { get; set; } + public bool CanPostPosts { get; set; } + public bool CanPostEvents { get; set; } + public bool CanMakeReposts { get; set; } + public bool CanAddCommentsToPosts { get; set; } + public bool CanAddReactionsToPosts { get; set; } + public bool CanAddCommentsToEvents { get; set; } + public bool CanAddReactionsToEvents { get; set; } + public bool DisplayFeedInMainOrganization { get; set; } + + public OrganizationSettingsDto() + { + + } + + public OrganizationSettingsDto(OrganizationSettings settings) + { + IsVisible = settings.IsVisible; + IsPublic = settings.IsPublic; + IsPrivate = settings.IsPrivate; + CanAddComments = settings.CanAddComments; + CanAddReactions = settings.CanAddReactions; + CanPostPosts = settings.CanPostPosts; + CanPostEvents = settings.CanPostEvents; + CanMakeReposts = settings.CanMakeReposts; + CanAddCommentsToPosts = settings.CanAddCommentsToPosts; + CanAddReactionsToPosts = settings.CanAddReactionsToPosts; + CanAddCommentsToEvents = settings.CanAddCommentsToEvents; + CanAddReactionsToEvents = settings.CanAddReactionsToEvents; + DisplayFeedInMainOrganization = settings.DisplayFeedInMainOrganization; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationUsersDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationUsersDto.cs new file mode 100644 index 000000000..abbba9ccd --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationUsersDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + + [ExcludeFromCodeCoverage] + public class OrganizationUsersDto + { + public OrganizationDto Organization { get; set; } + public IEnumerable Users { get; set; } + + public OrganizationUsersDto(Organization organization, IEnumerable users) + { + Organization = new OrganizationDto(organization); + Users = users.Select(u => new UserDto(u)).ToList(); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/RoleDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/RoleDto.cs new file mode 100644 index 000000000..5de3d1d53 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/RoleDto.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class RoleDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public Dictionary Permissions { get; set; } + + public RoleDto() + { + + } + + public RoleDto(Role role) + { + Id = role.Id; + Name = role.Name; + Description = role.Description; + Permissions = role.Permissions; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/SubOrganizationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/SubOrganizationDto.cs new file mode 100644 index 000000000..751c908cb --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/SubOrganizationDto.cs @@ -0,0 +1,31 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class SubOrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + + public SubOrganizationDto() + { + } + + public SubOrganizationDto(Organization organization) + { + Id = organization.Id; + Name = organization.Name; + Description = organization.Description; + BannerUrl = organization.BannerUrl; + ImageUrl = organization.ImageUrl; + OwnerId = organization.OwnerId; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/UserDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/UserDto.cs new file mode 100644 index 000000000..0cba4fd11 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/UserDto.cs @@ -0,0 +1,23 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class UserDto + { + public Guid Id { get; set; } + public RoleDto Role { get; set; } + + public UserDto() + { + } + + public UserDto(User user) + { + Id = user.Id; + Role = new RoleDto(user.Role); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 000000000..51a89283b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,62 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Core.Repositories; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler : IEventHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationGalleryRepository _organizationGalleryRepository; + + public MediaFileDeletedHandler( + IOrganizationRepository organizationRepository, + IOrganizationGalleryRepository organizationGalleryRepository) + { + _organizationRepository = organizationRepository; + _organizationGalleryRepository = organizationGalleryRepository; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine("Received MediaFileDeleted event: " + eventJson); + + if (@event.OrganizationId == null) + { + return; // If there's no OrganizationId, this event is not relevant to the organization service. + } + + var organization = await _organizationRepository.GetAsync(@event.OrganizationId.Value); + if (organization == null) + { + return; // Organization not found + } + + // Determine the type of media file that was deleted and update the organization accordingly. + if (@event.Source == "OrganizationProfileImage") + { + if (organization.ImageUrl == @event.MediaFileUrl) + { + organization.SetProfileImage(null); + await _organizationRepository.UpdateAsync(organization); + } + } + else if (@event.Source == "OrganizationBannerImage") + { + if (organization.BannerUrl == @event.MediaFileUrl) + { + organization.SetBannerImage(null); + await _organizationRepository.UpdateAsync(organization); + } + } + else if (@event.Source == "OrganizationGalleryImage") + { + // Remove the image from the organization's gallery if it exists + await _organizationGalleryRepository.RemoveImageAsync(@event.OrganizationId.Value, @event.MediaFileUrl); + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizationImageUploadedHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizationImageUploadedHandler.cs new file mode 100644 index 000000000..b95c63ef2 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizationImageUploadedHandler.cs @@ -0,0 +1,47 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Core.Entities; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Application.Events.External.Handlers +{ + public class OrganizationImageUploadedHandler : IEventHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationGalleryRepository _organizationGalleryRepository; + + public OrganizationImageUploadedHandler( + IOrganizationRepository organizationRepository, + IOrganizationGalleryRepository organizationGalleryRepository) + { + _organizationRepository = organizationRepository; + _organizationGalleryRepository = organizationGalleryRepository; + } + + public async Task HandleAsync(OrganizationImageUploaded @event, CancellationToken cancellationToken) + { + var organization = await _organizationRepository.GetAsync(@event.OrganizationId); + if (organization == null) + { + return; + } + + if (@event.ImageType == "OrganizationProfileImage") + { + organization.SetProfileImage(@event.ImageUrl); + await _organizationRepository.UpdateAsync(organization); + } + else if (@event.ImageType == "OrganizationBannerImage") + { + organization.SetBannerImage(@event.ImageUrl); + await _organizationRepository.UpdateAsync(organization); + } + else if (@event.ImageType == "OrganizationGalleryImage") + { + var galleryImage = new GalleryImage(new Guid(), @event.ImageUrl, @event.UploadDate); + await _organizationGalleryRepository.AddImageAsync(@event.OrganizationId, galleryImage); + } + } + } +} 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 deleted file mode 100644 index c267c0b4b..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index bf50906d4..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -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/MediaFileDeleted.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 000000000..b8215559c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,26 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Organizations.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted : IEvent + { + public string MediaFileUrl { get; } + public Guid SourceId { get; } + public string Source { get; } + public Guid UploaderId { get; } + public Guid? OrganizationId { get; } + + public MediaFileDeleted(string mediaFileUrl, Guid sourceId, string source, + Guid uploaderId, Guid? organizationId) + { + MediaFileUrl = mediaFileUrl; + SourceId = sourceId; + Source = source; + UploaderId = uploaderId; + OrganizationId = organizationId; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizationImageUploaded.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizationImageUploaded.cs new file mode 100644 index 000000000..e9cb95ce9 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizationImageUploaded.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Organizations.Application.Events.External +{ + [Message("mediafiles")] + public class OrganizationImageUploaded : IEvent + { + public Guid OrganizationId { get; } + public string ImageUrl { get; } + public string ImageType { get; } + public DateTime UploadDate { get; } + + public OrganizationImageUploaded(Guid organizationId, string imageUrl, string imageType, DateTime uploadDate) + { + OrganizationId = organizationId; + ImageUrl = imageUrl; + ImageType = imageType; + UploadDate = uploadDate; + } + } +} 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 deleted file mode 100644 index 771c3bb87..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsGranted.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 90c7a2f4d..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/External/OrganizerRightsRevoked.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/OrganizationCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs index 7eebf8d42..a63495c5e 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationCreated.cs @@ -1,18 +1,27 @@ using Convey.CQRS.Events; +using System; namespace MiniSpace.Services.Organizations.Application.Events { - public class OrganizationCreated: IEvent + public class OrganizationCreated : IEvent { public Guid OrganizationId { get; } public string Name { get; } - public Guid ParentId { get; } + public string Description { get; } + public Guid? RootId { get; } + public Guid? ParentId { get; } + public Guid OwnerId { get; } + public DateTime CreatedAt { get; } - public OrganizationCreated(Guid organizationId, string name, Guid parentId) + public OrganizationCreated(Guid organizationId, string name, string description, Guid? rootId, Guid? parentId, Guid ownerId, DateTime createdAt) { OrganizationId = organizationId; Name = name; + Description = description; + RootId = rootId; ParentId = parentId; + OwnerId = ownerId; + CreatedAt = createdAt; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationSettingsUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationSettingsUpdated.cs new file mode 100644 index 000000000..8f053cb11 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationSettingsUpdated.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Organizations.Core.Entities; +using System; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class OrganizationSettingsUpdated : IEvent + { + public Guid OrganizationId { get; } + public OrganizationSettings Settings { get; } + public DateTime UpdatedAt { get; } + + public OrganizationSettingsUpdated(Guid organizationId, OrganizationSettings settings, DateTime updatedAt) + { + OrganizationId = organizationId; + Settings = settings; + UpdatedAt = updatedAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationUpserted.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationUpserted.cs new file mode 100644 index 000000000..b4c67acd1 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/OrganizationUpserted.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class OrganizationUpserted : IEvent + { + public Guid OrganizationId { get; } + public bool WasUpdated { get; } + public DateTime Timestamp { get; } + + public OrganizationUpserted(Guid organizationId, bool wasUpdated, DateTime timestamp) + { + OrganizationId = organizationId; + WasUpdated = wasUpdated; + Timestamp = timestamp; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RoleAssignedToMember.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RoleAssignedToMember.cs new file mode 100644 index 000000000..d27fe3d4e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RoleAssignedToMember.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class RoleAssignedToMember : IEvent + { + public Guid OrganizationId { get; } + public Guid MemberId { get; } + public string Role { get; } + public DateTime AssignedAt { get; } + + public RoleAssignedToMember(Guid organizationId, Guid memberId, string role, DateTime assignedAt) + { + OrganizationId = organizationId; + MemberId = memberId; + Role = role; + AssignedAt = assignedAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs index c22ace7ba..2a67ec497 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/RootOrganizationCreated.cs @@ -1,16 +1,19 @@ using Convey.CQRS.Events; +using System; namespace MiniSpace.Services.Organizations.Application.Events { - public class RootOrganizationCreated: IEvent + public class RootOrganizationCreated : IEvent { public Guid OrganizationId { get; } public string Name { get; } + public DateTime CreatedAt { get; } - public RootOrganizationCreated(Guid organizationId, string name) + public RootOrganizationCreated(Guid organizationId, string name, DateTime createdAt) { OrganizationId = organizationId; Name = name; + CreatedAt = createdAt; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/UserInvitedToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/UserInvitedToOrganization.cs new file mode 100644 index 000000000..06be75b28 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Events/UserInvitedToOrganization.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Organizations.Application.Events +{ + public class UserInvitedToOrganization : IEvent + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + public DateTime InvitedAt { get; } + + public UserInvitedToOrganization(Guid organizationId, Guid userId, DateTime invitedAt) + { + OrganizationId = organizationId; + UserId = userId; + InvitedAt = invitedAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidPermissionException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidPermissionException.cs new file mode 100644 index 000000000..a24289cbf --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/InvalidPermissionException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class InvalidPermissionException : AppException + { + public override string Code { get; } = "invalid_permission"; + public string Permission { get; } + + public InvalidPermissionException(string permission) : base($"Invalid permission: {permission}.") + { + Permission = permission; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/MemberNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/MemberNotFoundException.cs new file mode 100644 index 000000000..0a1d641ba --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/MemberNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class MemberNotFoundException : AppException + { + public override string Code { get; } = "member_not_found"; + public Guid MemberId { get; } + + public MemberNotFoundException(Guid memberId) : base($"Member with ID: {memberId} was not found.") + { + MemberId = memberId; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RoleNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RoleNotFoundException.cs new file mode 100644 index 000000000..ffff5189f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/RoleNotFoundException.cs @@ -0,0 +1,19 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class RoleNotFoundException : AppException + { + public override string Code { get; } = "role_not_found"; + public Guid RoleId { get; } + public string RoleName { get; } + + public RoleNotFoundException(Guid roleId) : base($"Role with id '{roleId}' was not found.") + { + RoleId = roleId; + } + + public RoleNotFoundException(string roleName) : base($"Role with name '{roleName}' was not found.") + { + RoleName = roleName; + } + } +} 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 deleted file mode 100644 index e756e940a..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UnauthorizedAccessException.cs +++ /dev/null @@ -1,13 +0,0 @@ -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/Exceptions/UserUnauthorizedAccessException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UserUnauthorizedAccessException.cs new file mode 100644 index 000000000..43352038a --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Exceptions/UserUnauthorizedAccessException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Organizations.Application.Exceptions +{ + public class UserUnauthorizedAccessException : AppException + { + public override string Code { get; } = "unauthorized_access"; + public string Resource { get; } + + public UserUnauthorizedAccessException(string resource) + : base($"Unauthorized access to resource: '{resource}'.") + { + Resource = resource; + } + } +} 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 index d39c8f60f..147f1c429 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganization.cs @@ -8,6 +8,5 @@ namespace MiniSpace.Services.Organizations.Application.Queries public class GetOrganization : IQuery { public Guid OrganizationId { get; set; } - public Guid RootId { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationRoles.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationRoles.cs new file mode 100644 index 000000000..2b6e76059 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationRoles.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetOrganizationRoles : IQuery> + { + public Guid OrganizationId { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGallery.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGallery.cs new file mode 100644 index 000000000..717040216 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGallery.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + + [ExcludeFromCodeCoverage] + public class GetOrganizationWithGallery : IQuery + { + public Guid OrganizationId { get; set; } + } + +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGalleryAndUsers.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGalleryAndUsers.cs new file mode 100644 index 000000000..b77056d08 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithGalleryAndUsers.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetOrganizationWithGalleryAndUsers : IQuery + { + public Guid OrganizationId { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithUsers.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithUsers.cs new file mode 100644 index 000000000..a8f2b3094 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetOrganizationWithUsers.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + + [ExcludeFromCodeCoverage] + public class GetOrganizationWithUsers : IQuery + { + public Guid OrganizationId { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserOrganizations.cs new file mode 100644 index 000000000..cebef17e8 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserOrganizations.cs @@ -0,0 +1,12 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetUserOrganizations : IQuery> + { + public Guid UserId { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/FeedItem.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/FeedItem.cs new file mode 100644 index 000000000..0bfbb8a7c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/FeedItem.cs @@ -0,0 +1,20 @@ +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class FeedItem + { + public Guid Id { get; } + public Guid OrganizationId { get; } + public Guid CreatorId { get; } + public string Content { get; } + public DateTime CreatedAt { get; } + + public FeedItem(Guid organizationId, Guid creatorId, string content) + { + Id = Guid.NewGuid(); + OrganizationId = organizationId; + CreatorId = creatorId; + Content = content; + CreatedAt = DateTime.UtcNow; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/GalleryImage.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/GalleryImage.cs new file mode 100644 index 000000000..ef5eb2653 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/GalleryImage.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class GalleryImage + { + public Guid ImageId { get; private set; } + public string ImageUrl { get; private set; } + public DateTime DateAdded { get; private set; } + + public GalleryImage(Guid imageId, string imageUrl, DateTime dateAdded) + { + ImageId = imageId; + ImageUrl = imageUrl ?? throw new ArgumentNullException(nameof(imageUrl)); + DateAdded = dateAdded; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Invitation.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Invitation.cs index bda1726f7..56d2c8ff9 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Invitation.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Invitation.cs @@ -3,12 +3,11 @@ namespace MiniSpace.Services.Organizations.Core.Entities public class Invitation { public Guid UserId { get; } - public string Email { get; } - public Invitation(Guid userId, string email) + public Invitation(Guid userId) { + UserId = userId; - Email = email; } } -} \ No newline at end of file +} 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 index a0c1b3c71..5548bdd44 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organization.cs @@ -1,4 +1,5 @@ using MiniSpace.Services.Organizations.Core.Exceptions; +using MiniSpace.Services.Organizations.Core.Events; using System; using System.Collections.Generic; using System.Linq; @@ -7,18 +8,20 @@ namespace MiniSpace.Services.Organizations.Core.Entities { public class Organization : AggregateRoot { - private ISet _organizers = new HashSet(); private ISet _subOrganizations = new HashSet(); private ISet _invitations = new HashSet(); private ISet _users = new HashSet(); - public string Name { get; private set; } - public bool IsPublic { get; private set; } + private ISet _roles = new HashSet(); + private ISet _gallery = new HashSet(); - public IEnumerable Organizers - { - get => _organizers; - private set => _organizers = new HashSet(value); - } + public string Name { get; private set; } + public string Description { get; private set; } + public OrganizationSettings Settings { get; private set; } + public string BannerUrl { get; private set; } + public string ImageUrl { get; private set; } + public Guid OwnerId { get; private set; } + public Guid? ParentOrganizationId { get; private set; } + public string DefaultRoleName { get; private set; } public IEnumerable SubOrganizations { @@ -38,56 +41,292 @@ public IEnumerable Users private set => _users = new HashSet(value); } - public Organization(Guid id, string name, bool isPublic, IEnumerable organizationOrganizers = null, - IEnumerable organizations = null) + public IEnumerable Roles + { + get => _roles; + private set => _roles = new HashSet(value); + } + + public IEnumerable Gallery + { + get => _gallery; + private set => _gallery = new HashSet(value); + } + + public Organization(Guid id, + string name, + string description, + OrganizationSettings settings, + Guid ownerId, + string bannerUrl = null, + string imageUrl = null, + Guid? parentOrganizationId = null, + IEnumerable organizations = null, + string defaultRoleName = "User") { Id = id; Name = name; - IsPublic = isPublic; - Organizers = organizationOrganizers ?? Enumerable.Empty(); + Description = description; + Settings = settings; + BannerUrl = bannerUrl; + ImageUrl = imageUrl; + OwnerId = ownerId; + ParentOrganizationId = parentOrganizationId; SubOrganizations = organizations ?? Enumerable.Empty(); + DefaultRoleName = defaultRoleName; + AddEvent(new OrganizationCreated(Id, Name, Description, id, ParentOrganizationId ?? Guid.Empty, OwnerId, DateTime.UtcNow)); + InitializeDefaultRoles(); + } + + + private void InitializeDefaultRoles() + { + _roles.Add(new Role("Creator", "Default role with all permissions for the creator.", GetDefaultPermissionsForCreator())); + _roles.Add(new Role("Admin", "Default role with administrative permissions.", GetDefaultPermissionsForAdmin())); + _roles.Add(new Role("Moderator", "Default role with moderation permissions.", GetDefaultPermissionsForModerator())); + _roles.Add(new Role("User", "Default role with basic user permissions.", GetDefaultPermissionsForUser())); } - public void AddOrganizer(Guid organizerId) + private Dictionary GetDefaultPermissionsForCreator() { - if (Organizers.Any(x => x.Id == organizerId)) + return new Dictionary { - throw new OrganizerAlreadyAddedToOrganizationException(organizerId, Id); - } - _organizers.Add(new Organizer(organizerId)); + { Permission.CreateSubGroups, true }, + { Permission.DeleteSubGroups, true }, + { Permission.EditOrganizationDetails, true }, + { Permission.InviteUsers, true }, + { Permission.RemoveMembers, true }, + { Permission.ManageMembershipRequests, true }, + { Permission.MakePosts, true }, + { Permission.EditPosts, true }, + { Permission.DeletePosts, true }, + { Permission.CommentOnPosts, true }, + { Permission.DeleteComments, true }, + { Permission.MakeReactions, true }, + { Permission.PinPosts, true }, + { Permission.CreateEvents, true }, + { Permission.EditEvents, true }, + { Permission.DeleteEvents, true }, + { Permission.ManageEventParticipation, true }, + { Permission.AssignRoles, true }, + { Permission.EditPermissions, true }, + { Permission.ViewAuditLogs, true }, + { Permission.SendMessageToAll, true }, + { Permission.CreateCommunicationChannels, true }, + { Permission.ManageFeed, true }, + { Permission.ModerateContent, true }, + { Permission.ModifyGallery, true }, + { Permission.UpdateProfileImage, true }, + { Permission.UpdateOrganizationImage, true } + }; } - public void RemoveOrganizer(Guid organizerId) + private Dictionary GetDefaultPermissionsForAdmin() { - var organizer = _organizers.SingleOrDefault(x => x.Id == organizerId); - if (organizer is null) + return new Dictionary { - throw new OrganizerIsNotInOrganization(organizerId, Id); - } - _organizers.Remove(organizer); + { Permission.CreateSubGroups, true }, + { Permission.DeleteSubGroups, true }, + { Permission.EditOrganizationDetails, true }, + { Permission.InviteUsers, true }, + { Permission.RemoveMembers, true }, + { Permission.ManageMembershipRequests, true }, + { Permission.MakePosts, true }, + { Permission.EditPosts, true }, + { Permission.DeletePosts, true }, + { Permission.CommentOnPosts, true }, + { Permission.DeleteComments, true }, + { Permission.MakeReactions, true }, + { Permission.PinPosts, true }, + { Permission.CreateEvents, true }, + { Permission.EditEvents, true }, + { Permission.DeleteEvents, true }, + { Permission.ManageEventParticipation, true }, + { Permission.AssignRoles, true }, + { Permission.EditPermissions, true }, + { Permission.ViewAuditLogs, true }, + { Permission.SendMessageToAll, true }, + { Permission.CreateCommunicationChannels, true }, + { Permission.ManageFeed, true }, + { Permission.ModerateContent, true }, + { Permission.ModifyGallery, true }, + { Permission.UpdateProfileImage, true }, + { Permission.UpdateOrganizationImage, true } + }; } - public void InviteUser(Guid userId, string email) + private Dictionary GetDefaultPermissionsForModerator() { + return new Dictionary + { + { Permission.MakePosts, true }, + { Permission.EditPosts, true }, + { Permission.DeletePosts, true }, + { Permission.CommentOnPosts, true }, + { Permission.DeleteComments, true }, + { Permission.MakeReactions, true }, + { Permission.PinPosts, true }, + { Permission.CreateEvents, true }, + { Permission.EditEvents, true }, + { Permission.DeleteEvents, true }, + { Permission.ManageEventParticipation, true }, + { Permission.ModerateContent, true }, + { Permission.ModifyGallery, true } + }; + } + + private Dictionary GetDefaultPermissionsForUser() + { + return new Dictionary + { + { Permission.MakePosts, true }, + { Permission.CommentOnPosts, true }, + { Permission.MakeReactions, true } + }; + } + + public void InviteUser(Guid userId) + { + var user = _users.SingleOrDefault(u => u.Id == OwnerId); + if (user == null || !user.HasPermission(Permission.InviteUsers)) + { + throw new UnauthorizedAccessException("User does not have permission to invite users."); + } + if (_invitations.Any(i => i.UserId == userId)) { throw new UserAlreadyInvitedException(userId, Id); } - _invitations.Add(new Invitation(userId, email)); + _invitations.Add(new Invitation(userId)); + AddEvent(new UserInvitedToOrganization(Id, userId, DateTime.UtcNow)); + } + + public void SetPrivacy(bool isPublic) + { + Settings.SetPrivacy(isPublic); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void UpdateSettings(OrganizationSettings settings) + { + Settings = settings; + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetVisibility(bool isVisible) + { + Settings.SetVisibility(isVisible); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetIsPrivate(bool isPrivate) + { + Settings.SetIsPrivate(isPrivate); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddComments(bool canAddComments) + { + Settings.SetCanAddComments(canAddComments); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddReactions(bool canAddReactions) + { + Settings.SetCanAddReactions(canAddReactions); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanPostPosts(bool canPostPosts) + { + Settings.SetCanPostPosts(canPostPosts); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanPostEvents(bool canPostEvents) + { + Settings.SetCanPostEvents(canPostEvents); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanMakeReposts(bool canMakeReposts) + { + Settings.SetCanMakeReposts(canMakeReposts); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddCommentsToPosts(bool canAddCommentsToPosts) + { + Settings.SetCanAddCommentsToPosts(canAddCommentsToPosts); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddReactionsToPosts(bool canAddReactionsToPosts) + { + Settings.SetCanAddReactionsToPosts(canAddReactionsToPosts); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddCommentsToEvents(bool canAddCommentsToEvents) + { + Settings.SetCanAddCommentsToEvents(canAddCommentsToEvents); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetCanAddReactionsToEvents(bool canAddReactionsToEvents) + { + Settings.SetCanAddReactionsToEvents(canAddReactionsToEvents); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void SetDisplayFeedInMainOrganization(bool displayFeedInMainOrganization) + { + Settings.SetDisplayFeedInMainOrganization(displayFeedInMainOrganization); + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + public void AssignRole(Guid memberId, string roleName) + { + var user = _users.SingleOrDefault(u => u.Id == memberId); + if (user == null) + { + throw new UserNotFoundException(memberId); + } + + var role = _roles.SingleOrDefault(r => r.Name == roleName); + if (role == null) + { + throw new RoleNotFoundException(roleName); + } + + // Assuming User entity has a method to assign roles + user.AssignRole(role); + + AddEvent(new RoleAssignedToUser(Id, memberId, roleName)); } - public void SignUpUser(Guid userId) + public void UpdateRolePermissions(Guid roleId, Dictionary permissions) { - if (_users.Any(u => u.Id == userId)) + var role = _roles.SingleOrDefault(r => r.Id == roleId); + if (role == null) { - throw new UserAlreadySignedUpException(userId, Id); + throw new RoleNotFoundException(roleId); } - _users.Add(new User(userId)); + role.UpdatePermissions(permissions); + AddEvent(new RolePermissionsUpdated(Id, roleId, permissions)); } - public void SetPrivacy(bool isPublic) + public void UpdateRole(Guid roleId, string newName, string newDescription, Dictionary newPermissions) { - IsPublic = isPublic; + var role = _roles.SingleOrDefault(r => r.Id == roleId); + if (role == null) + { + throw new RoleNotFoundException(roleId); + } + role.UpdateName(newName); + role.UpdateDescription(newDescription); + role.UpdatePermissions(newPermissions); + AddEvent(new RoleUpdated(Id, roleId, newName, newDescription, newPermissions)); } public Organization GetSubOrganization(Guid id) @@ -109,27 +348,67 @@ public Organization GetSubOrganization(Guid id) return null; } + public void AddRole(Role role) + { + if (_roles.Any(r => r.Name == role.Name)) + { + throw new RoleAlreadyExistsException(role.Name); + } + + _roles.Add(role); + AddEvent(new RoleCreated(Id, role.Id, role.Name, role.Description, role.Permissions)); + } + public void AddSubOrganization(Organization organization) => _subOrganizations.Add(organization); - public static List FindOrganizations(Guid targetOrganizerId, Organization rootOrganization) + public void AddGalleryImage(GalleryImage image) + { + _gallery.Add(image); + AddEvent(new GalleryImageAdded(Id, image.ImageId, image.ImageUrl, DateTime.UtcNow)); + } + + public void RemoveGalleryImage(Guid imageId) + { + var image = _gallery.SingleOrDefault(g => g.ImageId == imageId); + if (image == null) + { + throw new GalleryImageNotFoundException(imageId); + } + _gallery.Remove(image); + AddEvent(new GalleryImageRemoved(Id, imageId, DateTime.UtcNow)); + } + + public void UpdateBannerUrl(string bannerUrl) + { + BannerUrl = bannerUrl; + AddEvent(new OrganizationBannerUrlUpdated(Id, bannerUrl, DateTime.UtcNow)); + } + + public void UpdateImageUrl(string imageUrl) + { + ImageUrl = imageUrl; + AddEvent(new OrganizationImageUrlUpdated(Id, imageUrl, DateTime.UtcNow)); + } + + public static List FindOrganizations(Guid targetUserId, Organization rootOrganization) { var organizations = new List(); - FindOrganizationsRecursive(targetOrganizerId, rootOrganization, organizations); + FindOrganizationsRecursive(targetUserId, rootOrganization, organizations); return organizations; } - private static void FindOrganizationsRecursive(Guid targetOrganizerId, Organization currentOrganization, + private static void FindOrganizationsRecursive(Guid targetUserId, Organization currentOrganization, ICollection organizations) { - if (currentOrganization.Organizers.Any(x => x.Id == targetOrganizerId)) + if (currentOrganization.Users.Any(x => x.Id == targetUserId)) { organizations.Add(currentOrganization); } foreach (var subOrg in currentOrganization.SubOrganizations) { - FindOrganizationsRecursive(targetOrganizerId, subOrg, organizations); + FindOrganizationsRecursive(targetUserId, subOrg, organizations); } } @@ -179,7 +458,53 @@ public void RemoveChildOrganization(Organization organization) } parent._subOrganizations.Remove(organization); } - } - + public void UpdateDetails(string name, string description, OrganizationSettings settings, string bannerUrl, string imageUrl) + { + if (name != null && Name != name) + { + Name = name; + AddEvent(new OrganizationNameUpdated(Id, Name)); + } + + if (description != null && Description != description) + { + Description = description; + AddEvent(new OrganizationDescriptionUpdated(Id, Description)); + } + + if (settings != null && !Settings.Equals(settings)) + { + Settings = settings; + AddEvent(new OrganizationSettingsUpdated(Id, Settings)); + } + + if (bannerUrl != null && BannerUrl != bannerUrl) + { + BannerUrl = bannerUrl; + AddEvent(new OrganizationBannerUrlUpdated(Id, BannerUrl, DateTime.UtcNow)); + } + + if (imageUrl != null && ImageUrl != imageUrl) + { + ImageUrl = imageUrl; + AddEvent(new OrganizationImageUrlUpdated(Id, ImageUrl, DateTime.UtcNow)); + } + } + + public void SetProfileImage(string imageUrl) + { + ImageUrl = imageUrl; + } + + public void SetBannerImage(string imageUrl) + { + BannerUrl = imageUrl; + } + + public void UpdateDefaultRole(string roleName) + { + DefaultRoleName = roleName; + } + } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationSettings.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationSettings.cs new file mode 100644 index 000000000..d1143acb7 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationSettings.cs @@ -0,0 +1,114 @@ +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class OrganizationSettings + { + public bool IsVisible { get; private set; } + public bool IsPublic { get; private set; } + public bool IsPrivate { get; private set; } + public bool CanAddComments { get; private set; } + public bool CanAddReactions { get; private set; } + public bool CanPostPosts { get; private set; } + public bool CanPostEvents { get; private set; } + public bool CanMakeReposts { get; private set; } + public bool CanAddCommentsToPosts { get; private set; } + public bool CanAddReactionsToPosts { get; private set; } + public bool CanAddCommentsToEvents { get; private set; } + public bool CanAddReactionsToEvents { get; private set; } + public bool DisplayFeedInMainOrganization { get; private set; } + + public OrganizationSettings( + bool isVisible = true, + bool isPublic = true, + bool isPrivate = false, + bool canAddComments = true, + bool canAddReactions = true, + bool canPostPosts = true, + bool canPostEvents = true, + bool canMakeReposts = true, + bool canAddCommentsToPosts = true, + bool canAddReactionsToPosts = true, + bool canAddCommentsToEvents = true, + bool canAddReactionsToEvents = true, + bool displayFeedInMainOrganization = true) + { + IsVisible = isVisible; + IsPublic = isPublic; + IsPrivate = isPrivate; + CanAddComments = canAddComments; + CanAddReactions = canAddReactions; + CanPostPosts = canPostPosts; + CanPostEvents = canPostEvents; + CanMakeReposts = canMakeReposts; + CanAddCommentsToPosts = canAddCommentsToPosts; + CanAddReactionsToPosts = canAddReactionsToPosts; + CanAddCommentsToEvents = canAddCommentsToEvents; + CanAddReactionsToEvents = canAddReactionsToEvents; + DisplayFeedInMainOrganization = displayFeedInMainOrganization; + } + + public void SetVisibility(bool isVisible) + { + IsVisible = isVisible; + } + + public void SetPrivacy(bool isPublic) + { + IsPublic = isPublic; + } + + public void SetIsPrivate(bool isPrivate) + { + IsPrivate = isPrivate; + } + + public void SetCanAddComments(bool canAddComments) + { + CanAddComments = canAddComments; + } + + public void SetCanAddReactions(bool canAddReactions) + { + CanAddReactions = canAddReactions; + } + + public void SetCanPostPosts(bool canPostPosts) + { + CanPostPosts = canPostPosts; + } + + public void SetCanPostEvents(bool canPostEvents) + { + CanPostEvents = canPostEvents; + } + + public void SetCanMakeReposts(bool canMakeReposts) + { + CanMakeReposts = canMakeReposts; + } + + public void SetCanAddCommentsToPosts(bool canAddCommentsToPosts) + { + CanAddCommentsToPosts = canAddCommentsToPosts; + } + + public void SetCanAddReactionsToPosts(bool canAddReactionsToPosts) + { + CanAddReactionsToPosts = canAddReactionsToPosts; + } + + public void SetCanAddCommentsToEvents(bool canAddCommentsToEvents) + { + CanAddCommentsToEvents = canAddCommentsToEvents; + } + + public void SetCanAddReactionsToEvents(bool canAddReactionsToEvents) + { + CanAddReactionsToEvents = canAddReactionsToEvents; + } + + public void SetDisplayFeedInMainOrganization(bool displayFeedInMainOrganization) + { + DisplayFeedInMainOrganization = displayFeedInMainOrganization; + } + } +} 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 deleted file mode 100644 index 949e41f08..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Organizer.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/Entities/Permission.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Permission.cs new file mode 100644 index 000000000..508b114eb --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Permission.cs @@ -0,0 +1,33 @@ +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public enum Permission + { + CreateSubGroups, + DeleteSubGroups, + EditOrganizationDetails, + InviteUsers, + RemoveMembers, + ManageMembershipRequests, + MakePosts, + EditPosts, + DeletePosts, + CommentOnPosts, + DeleteComments, + MakeReactions, + PinPosts, + CreateEvents, + EditEvents, + DeleteEvents, + ManageEventParticipation, + AssignRoles, + EditPermissions, + ViewAuditLogs, + SendMessageToAll, + CreateCommunicationChannels, + ManageFeed, + ModerateContent, + ModifyGallery, + UpdateProfileImage, + UpdateOrganizationImage + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Role.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Role.cs new file mode 100644 index 000000000..1acd1c1bf --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/Role.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class Role + { + public Guid Id { get; } + public string Name { get; private set; } + public string Description { get; private set; } + public Dictionary Permissions { get; private set; } + + public Role(Guid id, string name, string description, Dictionary permissions) + { + Id = id; + Name = name; + Description = description; + Permissions = permissions; + } + + public Role(string name, string description, Dictionary permissions) + { + Id = Guid.NewGuid(); + Name = name; + Description = description; + Permissions = permissions; + } + + public void UpdatePermissions(Dictionary permissions) + { + Permissions = permissions; + } + + public void UpdateName(string name) + { + Name = name; + } + + public void UpdateDescription(string description) + { + Description = description; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/User.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/User.cs index 2ec477928..85a02a47d 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/User.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/User.cs @@ -3,11 +3,22 @@ namespace MiniSpace.Services.Organizations.Core.Entities public class User { public Guid Id { get; } + public Role Role { get; private set; } - public User(Guid id) + public User(Guid id, Role role) { Id = id; + Role = role ?? throw new ArgumentNullException(nameof(role)); + } + + public bool HasPermission(Permission permission) + { + return Role?.Permissions != null && Role.Permissions.ContainsKey(permission) && Role.Permissions[permission]; } - } -} \ No newline at end of file + public void AssignRole(Role role) + { + Role = role ?? throw new ArgumentNullException(nameof(role)); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageAdded.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageAdded.cs new file mode 100644 index 000000000..d2e89584f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageAdded.cs @@ -0,0 +1,20 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class GalleryImageAdded : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid ImageId { get; } + public string Url { get; } + public DateTime CreatedAt { get; } + + public GalleryImageAdded(Guid organizationId, Guid imageId, string url, DateTime createdAt) + { + OrganizationId = organizationId; + ImageId = imageId; + Url = url; + CreatedAt = createdAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageRemoved.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageRemoved.cs new file mode 100644 index 000000000..8b20fdd81 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/GalleryImageRemoved.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class GalleryImageRemoved : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid ImageId { get; } + public DateTime RemovedAt { get; } + + public GalleryImageRemoved(Guid organizationId, Guid imageId, DateTime removedAt) + { + OrganizationId = organizationId; + ImageId = imageId; + RemovedAt = removedAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationBannerUrlUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationBannerUrlUpdated.cs new file mode 100644 index 000000000..06d65d6a7 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationBannerUrlUpdated.cs @@ -0,0 +1,17 @@ +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationBannerUrlUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public string BannerUrl { get; } + public DateTime UpdatedAt { get; } + + public OrganizationBannerUrlUpdated(Guid organizationId, string bannerUrl, DateTime updatedAt) + { + OrganizationId = organizationId; + BannerUrl = bannerUrl; + UpdatedAt = updatedAt; + } + + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationCreated.cs new file mode 100644 index 000000000..728898300 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationCreated.cs @@ -0,0 +1,26 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationCreated : IDomainEvent + { + public Guid OrganizationId { get; } + public string Name { get; } + public string Description { get; } + public Guid RootId { get; } + public Guid ParentId { get; } + public Guid OwnerId { get; } + public DateTime CreatedAt { get; } + + public OrganizationCreated(Guid organizationId, string name, string description, Guid rootId, Guid parentId, Guid ownerId, DateTime createdAt) + { + OrganizationId = organizationId; + Name = name; + Description = description; + RootId = rootId; + ParentId = parentId; + OwnerId = ownerId; + CreatedAt = createdAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationDescriptionUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationDescriptionUpdated.cs new file mode 100644 index 000000000..a110312eb --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationDescriptionUpdated.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationDescriptionUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public string NewDescription { get; } + + public OrganizationDescriptionUpdated(Guid organizationId, string newDescription) + { + OrganizationId = organizationId; + NewDescription = newDescription; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationImageUrlUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationImageUrlUpdated.cs new file mode 100644 index 000000000..ac47e2806 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationImageUrlUpdated.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationImageUrlUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public string ImageUrl { get; } + public DateTime UpdatedAt { get; } + + public OrganizationImageUrlUpdated(Guid organizationId, string imageUrl, DateTime updatedAt) + { + OrganizationId = organizationId; + ImageUrl = imageUrl; + UpdatedAt = updatedAt; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationNameUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationNameUpdated.cs new file mode 100644 index 000000000..70ceb09d2 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationNameUpdated.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationNameUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public string NewName { get; } + + public OrganizationNameUpdated(Guid organizationId, string newName) + { + OrganizationId = organizationId; + NewName = newName; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationSettingsUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationSettingsUpdated.cs new file mode 100644 index 000000000..c9b3713ac --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/OrganizationSettingsUpdated.cs @@ -0,0 +1,17 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class OrganizationSettingsUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public OrganizationSettings Settings { get; } + + public OrganizationSettingsUpdated(Guid organizationId, OrganizationSettings settings) + { + OrganizationId = organizationId; + Settings = settings; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleAssignedToUser.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleAssignedToUser.cs new file mode 100644 index 000000000..f55a96aed --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleAssignedToUser.cs @@ -0,0 +1,17 @@ + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RoleAssignedToUser : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + public string Role { get; } + + public RoleAssignedToUser(Guid organizationId, Guid userId, string role) + { + OrganizationId = organizationId; + UserId = userId; + Role = role; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleCreated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleCreated.cs new file mode 100644 index 000000000..b581fb8ee --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleCreated.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RoleCreated : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string RoleName { get; } + public string Description { get; } + public Dictionary Permissions { get; } + + public RoleCreated(Guid organizationId, Guid roleId, string roleName, string description, Dictionary permissions) + { + OrganizationId = organizationId; + RoleId = roleId; + RoleName = roleName; + Description = description; + Permissions = permissions; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleDescriptionUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleDescriptionUpdated.cs new file mode 100644 index 000000000..ebc44ac85 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleDescriptionUpdated.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RoleDescriptionUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string NewDescription { get; } + + public RoleDescriptionUpdated(Guid organizationId, Guid roleId, string newDescription) + { + OrganizationId = organizationId; + RoleId = roleId; + NewDescription = newDescription; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleNameUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleNameUpdated.cs new file mode 100644 index 000000000..46f0dc706 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleNameUpdated.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RoleNameUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string NewName { get; } + + public RoleNameUpdated(Guid organizationId, Guid roleId, string newName) + { + OrganizationId = organizationId; + RoleId = roleId; + NewName = newName; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RolePermissionsUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RolePermissionsUpdated.cs new file mode 100644 index 000000000..5a4f79cf2 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RolePermissionsUpdated.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Events; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RolePermissionsUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public Dictionary Permissions { get; } + + public RolePermissionsUpdated(Guid organizationId, Guid roleId, Dictionary permissions) + { + OrganizationId = organizationId; + RoleId = roleId; + Permissions = permissions; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleUpdated.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleUpdated.cs new file mode 100644 index 000000000..450078317 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/RoleUpdated.cs @@ -0,0 +1,22 @@ +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class RoleUpdated : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string NewName { get; } + public string NewDescription { get; } + public Dictionary NewPermissions { get; } + + public RoleUpdated(Guid organizationId, Guid roleId, string newName, string newDescription, Dictionary newPermissions) + { + OrganizationId = organizationId; + RoleId = roleId; + NewName = newName; + NewDescription = newDescription; + NewPermissions = newPermissions; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserInvitedToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserInvitedToOrganization.cs new file mode 100644 index 000000000..ec81d0ff5 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserInvitedToOrganization.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class UserInvitedToOrganization : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + public DateTime InvitedAt { get; } + + public UserInvitedToOrganization(Guid organizationId, Guid userId, DateTime invitedAt) + { + OrganizationId = organizationId; + UserId = userId; + InvitedAt = invitedAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserSignedUpToOrganization.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserSignedUpToOrganization.cs new file mode 100644 index 000000000..3e1e3f608 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Events/UserSignedUpToOrganization.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Events +{ + public class UserSignedUpToOrganization : IDomainEvent + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + public DateTime SignedUpAt { get; } + + public UserSignedUpToOrganization(Guid organizationId, Guid userId, DateTime signedUpAt) + { + OrganizationId = organizationId; + UserId = userId; + SignedUpAt = signedUpAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/GalleryImageNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/GalleryImageNotFoundException.cs new file mode 100644 index 000000000..eb592b982 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/GalleryImageNotFoundException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class GalleryImageNotFoundException : DomainException + { + public override string Code { get; } = "gallery_image_not_found"; + public Guid ImageId { get; } + + public GalleryImageNotFoundException(Guid imageId) + : base($"Gallery image with ID: '{imageId}' was not found.") + { + ImageId = imageId; + } + } +} 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 deleted file mode 100644 index 8406b71fa..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/OrganizerAlreadyAddedToOrganizationException.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/RoleAlreadyExistsException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/RoleAlreadyExistsException.cs new file mode 100644 index 000000000..470559091 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/RoleAlreadyExistsException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class RoleAlreadyExistsException : DomainException + { + public override string Code { get; } = "role_already_exists"; + public string RoleName { get; } + + public RoleAlreadyExistsException(string roleName) : base($"Role '{roleName}' already exists.") + { + RoleName = roleName; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/RoleNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/RoleNotFoundException.cs new file mode 100644 index 000000000..be4c60960 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/RoleNotFoundException.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class RoleNotFoundException : DomainException + { + public override string Code { get; } = "role_not_found"; + public Guid RoleId { get; } + public String RoleName { get; } + + public RoleNotFoundException(Guid roleId) : base($"Role with ID: '{roleId}' was not found.") + { + RoleId = roleId; + } + public RoleNotFoundException(string roleName) : base($"Role with name: '{roleName}' was not found.") + { + RoleName = roleName; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserAlreadyInvitedException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserAlreadyInvitedException.cs index 0ffc6a767..1a422f5af 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserAlreadyInvitedException.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserAlreadyInvitedException.cs @@ -6,8 +6,8 @@ public class UserAlreadyInvitedException : DomainException public Guid UserId { get; } public Guid OrganizationId { get; } - public UserAlreadyInvitedException(Guid userId, Guid organizationId) - : base($"User with ID: '{userId}' has already been invited to organization with ID: '{organizationId}'.") + public UserAlreadyInvitedException(Guid userId, Guid organizationId) : base( + $"User with ID: '{userId}' has already been invited to organization with ID: '{organizationId}'.") { UserId = userId; OrganizationId = organizationId; diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserNotFoundException.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserNotFoundException.cs new file mode 100644 index 000000000..3251ee9ba --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Exceptions/UserNotFoundException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.Organizations.Core.Exceptions +{ + public class UserNotFoundException : DomainException + { + public override string Code { get; } = "user_not_found"; + public Guid UserId { get; } + + public UserNotFoundException(Guid userId) : base($"User with ID: '{userId}' was not found.") + { + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IGalleryRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IGalleryRepository.cs new file mode 100644 index 000000000..e2c9acf8c --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IGalleryRepository.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IGalleryRepository + { + Task GetImageAsync(Guid organizationId, Guid imageId); + Task> GetImagesAsync(Guid organizationId); + Task AddImageAsync(GalleryImage image); + Task RemoveImageAsync(Guid organizationId, Guid imageId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationGalleryRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationGalleryRepository.cs new file mode 100644 index 000000000..759144559 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationGalleryRepository.cs @@ -0,0 +1,17 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationGalleryRepository + { + Task GetImageAsync(Guid organizationId, Guid imageId); + Task> GetGalleryAsync(Guid organizationId); + Task AddImageAsync(Guid organizationId, GalleryImage image); + Task UpdateImageAsync(Guid organizationId, GalleryImage image); + Task DeleteImageAsync(Guid organizationId, Guid imageId); + Task RemoveImageAsync(Guid organizationId, string mediaFileUrl); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationMembersRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationMembersRepository.cs new file mode 100644 index 000000000..b90808bf3 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationMembersRepository.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationMembersRepository + { + Task GetMemberAsync(Guid organizationId, Guid memberId); + Task> GetMembersAsync(Guid organizationId); + Task AddMemberAsync(Guid organizationId, User member); + Task UpdateMemberAsync(Guid organizationId, User member); + Task DeleteMemberAsync(Guid organizationId, Guid memberId); + } +} 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 index 2e13079e1..5870dadf9 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRepository.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRepository.cs @@ -1,4 +1,7 @@ using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; namespace MiniSpace.Services.Organizations.Core.Repositories { @@ -9,5 +12,11 @@ public interface IOrganizationRepository Task AddAsync(Organization organization); Task UpdateAsync(Organization organization); Task DeleteAsync(Guid id); + + Task GetMemberAsync(Guid organizationId, Guid memberId); + Task> GetMembersAsync(Guid organizationId); + Task AddMemberAsync(Guid organizationId, User member); + Task UpdateMemberAsync(Guid organizationId, User member); + Task DeleteMemberAsync(Guid organizationId, Guid memberId); } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRolesRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRolesRepository.cs new file mode 100644 index 000000000..c0b012b72 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationRolesRepository.cs @@ -0,0 +1,17 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationRolesRepository + { + Task GetRoleAsync(Guid organizationId, Guid roleId); + Task> GetRolesAsync(Guid organizationId); + Task GetRoleByNameAsync(Guid organizationId, string roleName); + Task AddRoleAsync(Guid organizationId, Role role); + Task UpdateRoleAsync(Role role); + Task DeleteRoleAsync(Guid roleId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationSettingsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationSettingsRepository.cs new file mode 100644 index 000000000..41ecaf06b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationSettingsRepository.cs @@ -0,0 +1,12 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationSettingsRepository + { + Task GetAsync(Guid organizationId); + Task UpdateAsync(OrganizationSettings settings); + } +} 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 deleted file mode 100644 index 5140e97a8..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizerRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -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.Core/Repositories/IUserInvitationsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserInvitationsRepository.cs new file mode 100644 index 000000000..49a1b2316 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserInvitationsRepository.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IUserInvitationsRepository + { + Task GetInvitationAsync(Guid organizationId, Guid userId); + Task> GetInvitationsAsync(Guid organizationId); + Task AddInvitationAsync(Guid organizationId, Invitation invitation); + Task DeleteInvitationAsync(Guid organizationId, Guid userId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs index b24efa368..48828be82 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Extensions.cs @@ -27,9 +27,8 @@ 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.Commands; using MiniSpace.Services.Organizations.Application.Services; using MiniSpace.Services.Organizations.Core.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Contexts; @@ -49,7 +48,11 @@ public static class Extensions public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) { builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); @@ -74,7 +77,10 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddJaeger() .AddHandlersLogging() .AddMongoRepository("organizations") - .AddMongoRepository("organizers") + .AddMongoRepository("organization_gallery_images") + .AddMongoRepository("organization_members") + .AddMongoRepository("organization_invitations") + .AddMongoRepository("organization_roles") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); @@ -91,11 +97,19 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseCertificateAuthentication() .UseRabbitMq() .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() - .SubscribeCommand() - .SubscribeCommand() - .SubscribeEvent() - .SubscribeEvent(); + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeEvent() + .SubscribeEvent(); return app; } 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 index 82111a8f3..5471c9a02 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -1,6 +1,7 @@ using Convey.Logging.CQRS; using MiniSpace.Services.Organizations.Application.Commands; -using MiniSpace.Services.Organizations.Application.Events.External; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Logging @@ -12,45 +13,69 @@ private static IReadOnlyDictionary MessageTemplates => new Dictionary { { - typeof(CreateRootOrganization), new HandlerLogTemplate + typeof(CreateOrganization), new HandlerLogTemplate { - After = "Created a new root organization with id: {OrganizationId}." + After = "Added a new organization with id: {OrganizationId}, name: {Name}." } }, { - typeof(CreateOrganization), new HandlerLogTemplate + typeof(DeleteOrganization), new HandlerLogTemplate { - After = "Added a new child organization with id: {OrganizationId} for parent with id: {ParentId}." + After = "Deleted an organization with id: {OrganizationId} and its children." } }, { - typeof(DeleteOrganization), new HandlerLogTemplate + typeof(AssignRoleToMember), new HandlerLogTemplate { - After = "Deleted an organization with id: {OrganizationId} and its children." + After = "Assigned role '{Role}' to member with id: {MemberId} in organization with id: {OrganizationId}." + } + }, + { + typeof(CreateOrganizationRole), new HandlerLogTemplate + { + After = "Created a new role '{RoleName}' in organization with id: {OrganizationId}." + } + }, + { + typeof(CreateSubOrganization), new HandlerLogTemplate + { + After = "Created a new sub-organization with id: {SubOrganizationId} under parent organization with id: {ParentId}." + } + }, + { + typeof(InviteUserToOrganization), new HandlerLogTemplate + { + After = "Invited user with id: {UserId} to organization with id: {OrganizationId}." + } + }, + { + typeof(ManageFeed), new HandlerLogTemplate + { + After = "Performed '{Action}' action on the feed content in organization with id: {OrganizationId}." } }, { - typeof(AddOrganizerToOrganization), new HandlerLogTemplate + typeof(SetOrganizationPrivacy), new HandlerLogTemplate { - After = "Added an organizer with id: {OrganizerId} to the organization with id: {OrganizationId}." + After = "Set privacy of organization with id: {OrganizationId} to '{IsPrivate}'." } }, { - typeof(RemoveOrganizerFromOrganization), new HandlerLogTemplate + typeof(SetOrganizationVisibility), new HandlerLogTemplate { - After = "Removed an organizer with id: {OrganizerId} from the organization with id: {OrganizationId}." + After = "Set visibility of organization with id: {OrganizationId} to '{IsVisible}'." } }, { - typeof(OrganizerRightsGranted), new HandlerLogTemplate + typeof(UpdateOrganizationSettings), new HandlerLogTemplate { - After = "Created an organizer with id: {UserId}." + After = "Updated settings of organization with id: {OrganizationId}." } }, { - typeof(OrganizerRightsRevoked), new HandlerLogTemplate + typeof(UpdateRolePermissions), new HandlerLogTemplate { - After = "Deleted an organizer with id: {UserId}." + After = "Updated permissions for role with id: {RoleId} in organization with id: {OrganizationId}." } } }; 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 index 11e9f07c8..6be0d2dd1 100644 --- 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 @@ -1,49 +1,209 @@ using MiniSpace.Services.Organizations.Application.DTO; using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents { [ExcludeFromCodeCoverage] - public static class Extensions { public static Organization AsEntity(this OrganizationDocument document) - => new Organization(document.Id, document.Name, document.Organizers, document.SubOrganizations.Select(o => o.AsEntity())); - + => new Organization( + document.Id, + document.Name, + document.Description, + document.Settings, + document.OwnerId, + document.BannerUrl, + document.ImageUrl, + document.ParentOrganizationId, + document.SubOrganizations?.Select(o => o.AsEntity()), + document.DefaultRoleName + ); + public static OrganizationDocument AsDocument(this Organization entity) - => new OrganizationDocument() + => new OrganizationDocument { Id = entity.Id, Name = entity.Name, - Organizers = entity.Organizers, - SubOrganizations = entity.SubOrganizations.Select(o => o.AsDocument()) + Description = entity.Description, + Settings = entity.Settings, + BannerUrl = entity.BannerUrl, + ImageUrl = entity.ImageUrl, + OwnerId = entity.OwnerId, + ParentOrganizationId = entity.ParentOrganizationId, + SubOrganizations = entity.SubOrganizations?.Select(o => o.AsDocument()).ToList(), + DefaultRoleName = entity.DefaultRoleName }; - - public static OrganizationDto AsDto(this OrganizationDocument document, Guid rootId) - => new OrganizationDto() + + public static OrganizationDto AsDto(this OrganizationDocument document) + => new OrganizationDto { Id = document.Id, Name = document.Name, - RootId = rootId + Description = document.Description, + BannerUrl = document.BannerUrl, + ImageUrl = document.ImageUrl, + OwnerId = document.OwnerId, + DefaultRoleName = document.DefaultRoleName }; - - public static OrganizationDetailsDto AsDetailsDto(this OrganizationDocument document, Guid rootId) - => new OrganizationDetailsDto() + + public static OrganizationDetailsDto AsDetailsDto(this OrganizationDocument document) + => new OrganizationDetailsDto { Id = document.Id, Name = document.Name, - RootId = rootId, - Organizers = document.Organizers.Select(x => x.Id) + Description = document.Description, + BannerUrl = document.BannerUrl, + ImageUrl = document.ImageUrl, + OwnerId = document.OwnerId, + ParentOrganizationId = document.ParentOrganizationId, + SubOrganizations = document.SubOrganizations?.Select(o => new SubOrganizationDto + { + Id = o.Id, + Name = o.Name, + Description = o.Description, + BannerUrl = o.BannerUrl, + ImageUrl = o.ImageUrl, + OwnerId = o.OwnerId + }).ToList(), + DefaultRoleName = document.DefaultRoleName + }; + + public static OrganizationDto AsSubDto(this OrganizationDocument document) + => new OrganizationDto + { + Id = document.Id, + Name = document.Name, + Description = document.Description, + BannerUrl = document.BannerUrl, + ImageUrl = document.ImageUrl, + OwnerId = document.OwnerId, + DefaultRoleName = document.DefaultRoleName + }; + + public static Invitation AsEntity(this InvitationEntry document) + => new Invitation(document.UserId); + + public static InvitationEntry AsDocument(this Invitation entity) + => new InvitationEntry + { + UserId = entity.UserId + }; + + public static OrganizationInvitationDocument AsInvitationDocument(this IEnumerable entities, Guid organizationId) + => new OrganizationInvitationDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Invitations = entities.Select(e => e.AsDocument()).ToList() + }; + + public static IEnumerable AsInvitationEntities(this OrganizationInvitationDocument document) + => document.Invitations.Select(i => i.AsEntity()); + + public static Role AsEntity(this RoleEntry document) + { + return new Role( + document.Id, + document.Name, + document.Description, + document.Permissions.ToDictionary( + kvp => Enum.Parse(kvp.Key), + kvp => kvp.Value) + ); + } + + public static RoleEntry AsDocument(this Role entity) + { + return new RoleEntry + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + Permissions = entity.Permissions.ToDictionary( + kvp => kvp.Key.ToString(), + kvp => kvp.Value) + }; + } + + + public static OrganizationRolesDocument AsRoleDocument(this IEnumerable entities, Guid organizationId) + => new OrganizationRolesDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Roles = entities.Select(e => e.AsDocument()).ToList() + }; + + public static IEnumerable AsRoleEntities(this OrganizationRolesDocument document) + => document.Roles.Select(r => r.AsEntity()); + + + public static OrganizationGalleryImageDocument AsGalleryImageDocument(this IEnumerable entities, Guid organizationId) + => new OrganizationGalleryImageDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Gallery = entities.Select(e => e.AsDocument()).ToList() + }; + + public static IEnumerable AsGalleryImageEntities(this OrganizationGalleryImageDocument document) + => document.Gallery.Select(g => g.AsEntity()); + + + public static User AsEntity(this UserEntry document) + { + return new User( + document.UserId, + new Role( + document.Role.RoleId, + document.Role.RoleName, + string.Empty, + new Dictionary() + ) + ); + } + + + public static UserEntry AsDocument(this User entity) + => new UserEntry + { + UserId = entity.Id, + Role = new RoleAssignment + { + RoleId = entity.Role.Id, + RoleName = entity.Role.Name + } }; - - public static Organizer AsEntity(this OrganizerDocument document) - => new Organizer(document.Id); - - public static OrganizerDocument AsDocument(this Organizer entity) - => new OrganizerDocument() + + public static OrganizationMembersDocument AsUserDocument(this IEnumerable entities, Guid organizationId) + => new OrganizationMembersDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Users = entities.Select(e => e.AsDocument()).ToList() + }; + + public static IEnumerable AsUserEntities(this OrganizationMembersDocument document) + => document.Users.Select(u => u.AsEntity()); + + public static GalleryImage AsEntity(this GalleryImageEntry document) + { + return new GalleryImage(document.ImageId, document.ImageUrl, document.DateAdded); + } + + public static GalleryImageEntry AsDocument(this GalleryImage entity) + { + return new GalleryImageEntry { - Id = entity.Id + ImageId = entity.ImageId, + ImageUrl = entity.ImageUrl, + DateAdded = entity.DateAdded }; - } + } + } } 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 index 375f3a0f8..0c5cb0982 100644 --- 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 @@ -1,16 +1,23 @@ using Convey.Types; using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents { [ExcludeFromCodeCoverage] - - public class OrganizationDocument: IIdentifiable + public class OrganizationDocument : IIdentifiable { public Guid Id { get; set; } public string Name { get; set; } - public IEnumerable Organizers { get; set; } + public string Description { get; set; } + public OrganizationSettings Settings { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + public Guid? ParentOrganizationId { get; set; } public IEnumerable SubOrganizations { get; set; } + public string DefaultRoleName { get; set; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationGalleryImageDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationGalleryImageDocument.cs new file mode 100644 index 000000000..98271f7c6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationGalleryImageDocument.cs @@ -0,0 +1,22 @@ +using Convey.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class OrganizationGalleryImageDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable Gallery { get; set; } + } + + public class GalleryImageEntry + { + public Guid ImageId { get; set; } + public string ImageUrl { get; set; } + public DateTime DateAdded { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationInvitationDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationInvitationDocument.cs new file mode 100644 index 000000000..ecd45b692 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationInvitationDocument.cs @@ -0,0 +1,20 @@ +using Convey.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class OrganizationInvitationDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable Invitations { get; set; } + } + + public class InvitationEntry + { + public Guid UserId { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationMembersDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationMembersDocument.cs new file mode 100644 index 000000000..7e5bb38be --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationMembersDocument.cs @@ -0,0 +1,27 @@ +using Convey.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class OrganizationMembersDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable Users { get; set; } + } + + public class UserEntry + { + public Guid UserId { get; set; } + public RoleAssignment Role { get; set; } + } + + public class RoleAssignment + { + public Guid RoleId { get; set; } + public string RoleName { get; set; } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationRolesDocument.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationRolesDocument.cs new file mode 100644 index 000000000..0e644f8a3 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizationRolesDocument.cs @@ -0,0 +1,16 @@ +using Convey.Types; +using MiniSpace.Services.Organizations.Core.Entities; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class OrganizationRolesDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public IEnumerable Roles { get; set; } + } +} 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 deleted file mode 100644 index 0345c012a..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/OrganizerDocument.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Convey.Types; -using System.Diagnostics.CodeAnalysis; - -namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents -{ - [ExcludeFromCodeCoverage] - 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/Documents/RoleEntry.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/RoleEntry.cs new file mode 100644 index 000000000..1bbd79cc6 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Documents/RoleEntry.cs @@ -0,0 +1,39 @@ +using MiniSpace.Services.Organizations.Core.Entities; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents +{ + public class RoleEntry + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + [BsonDictionaryOptions(MongoDB.Bson.Serialization.Options.DictionaryRepresentation.Document)] + public Dictionary Permissions { get; set; } + + public static RoleEntry FromEntity(Role role) + { + return new RoleEntry + { + Id = role.Id, + Name = role.Name, + Description = role.Description, + Permissions = role.Permissions.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value) + }; + } + + public Role ToEntity() + { + return new Role( + Id, + Name, + Description, + Permissions.ToDictionary(kvp => Enum.Parse(kvp.Key), kvp => kvp.Value) + ); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs index 67babbb42..9862838ba 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetAllChildrenOrganizationsHandler.cs @@ -3,6 +3,10 @@ using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers @@ -28,4 +32,4 @@ public async Task> HandleAsync(GetAllChildrenOrganizations que return result; } } -} \ 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 index 28401dcdb..e7fe7dd86 100644 --- 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 @@ -2,8 +2,11 @@ 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers @@ -26,7 +29,7 @@ public async Task> HandleAsync(GetChildrenOrganizat var parent = root.AsEntity().GetSubOrganization(query.OrganizationId); return parent == null ? Enumerable.Empty() - : parent.SubOrganizations.Select(o => new OrganizationDto(o, root.Id)); + : parent.SubOrganizations.Select(o => new OrganizationDto(o)); } } -} \ 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 index d78c2a003..cfdd4e51f 100644 --- 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 @@ -23,7 +23,7 @@ public async Task HandleAsync(GetOrganizationDetails que { var root = await _repository.GetAsync(o => o.Id == query.RootId); var organization = root?.AsEntity().GetSubOrganization(query.OrganizationId); - return organization == null ? null : new OrganizationDetailsDto(organization, root.Id); + return organization == null ? null : new OrganizationDetailsDto(organization); } } } \ 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 index 8978aa5c7..0c649de62 100644 --- 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 @@ -9,7 +9,7 @@ namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers { [ExcludeFromCodeCoverage] - public class GetOrganizationHandler : IQueryHandler + public class GetOrganizationHandler : IQueryHandler { private readonly IMongoRepository _repository; @@ -20,9 +20,14 @@ public GetOrganizationHandler(IMongoRepository repos public async Task HandleAsync(GetOrganization query, CancellationToken cancellationToken) { - var root = await _repository.GetAsync(o => o.Id == query.RootId); - var organization = root?.AsEntity().GetSubOrganization(query.OrganizationId); - return organization == null ? null : new OrganizationDto(organization, root.Id); + var organizationDocument = await _repository.GetAsync(o => o.Id == query.OrganizationId); + if (organizationDocument == null) + { + return null; + } + + var organization = organizationDocument.AsEntity(); + return new OrganizationDto(organization); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationRolesHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationRolesHandler.cs new file mode 100644 index 000000000..8474b4781 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationRolesHandler.cs @@ -0,0 +1,29 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Repositories; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + [ExcludeFromCodeCoverage] + public class GetOrganizationRolesHandler : IQueryHandler> + { + private readonly IOrganizationRolesRepository _organizationRolesRepository; + + public GetOrganizationRolesHandler(IOrganizationRolesRepository organizationRolesRepository) + { + _organizationRolesRepository = organizationRolesRepository; + } + + public async Task> HandleAsync(GetOrganizationRoles query, CancellationToken cancellationToken) + { + var roles = await _organizationRolesRepository.GetRolesAsync(query.OrganizationId); + return roles?.Select(role => new RoleDto(role)) ?? Enumerable.Empty(); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryAndUsersHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryAndUsersHandler.cs new file mode 100644 index 000000000..ae917b228 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryAndUsersHandler.cs @@ -0,0 +1,71 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Repositories; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOrganizationWithGalleryAndUsersHandler : IQueryHandler + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationGalleryRepository _galleryRepository; + private readonly IOrganizationRolesRepository _organizationRolesRepository; + + public GetOrganizationWithGalleryAndUsersHandler( + IOrganizationRepository organizationRepository, + IOrganizationGalleryRepository galleryRepository, + IOrganizationRolesRepository organizationRolesRepository) + { + _organizationRepository = organizationRepository; + _galleryRepository = galleryRepository; + _organizationRolesRepository = organizationRolesRepository; + } + + public async Task HandleAsync(GetOrganizationWithGalleryAndUsers query, CancellationToken cancellationToken) + { + var organization = await _organizationRepository.GetAsync(query.OrganizationId); + if (organization == null) + { + return null; + } + + var galleryImages = await _galleryRepository.GetGalleryAsync(organization.Id); + + if (galleryImages == null) + { + Console.WriteLine("Gallery Images Retrieved: null"); + galleryImages = Enumerable.Empty(); + } + else + { + Console.WriteLine("Gallery Images Retrieved:"); + Console.WriteLine(JsonSerializer.Serialize(galleryImages, new JsonSerializerOptions { WriteIndented = true })); + } + + var roles = await _organizationRolesRepository.GetRolesAsync(organization.Id); + + var settingsDto = organization.Settings != null + ? new OrganizationSettingsDto(organization.Settings) + : new OrganizationSettingsDto(); + + var result = new OrganizationGalleryUsersDto(organization, galleryImages, organization.Users) + { + OrganizationDetails = new OrganizationDetailsDto(organization) + { + Settings = settingsDto, + Roles = roles?.Select(r => new RoleDto(r)).ToList() ?? new List() + }, + Gallery = galleryImages.Select(g => new GalleryImageDto(g)).ToList(), + Users = organization.Users?.Select(u => new UserDto(u)).ToList() ?? new List() + }; + + return result; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryHandler.cs new file mode 100644 index 000000000..6b5a9e162 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithGalleryHandler.cs @@ -0,0 +1,46 @@ +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 System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + + + [ExcludeFromCodeCoverage] + public class GetOrganizationWithGalleryHandler : IQueryHandler + { + private readonly IMongoRepository _organizationRepository; + private readonly IMongoRepository _galleryRepository; + + public GetOrganizationWithGalleryHandler( + IMongoRepository organizationRepository, + IMongoRepository galleryRepository) + { + _organizationRepository = organizationRepository; + _galleryRepository = galleryRepository; + } + + public async Task HandleAsync(GetOrganizationWithGallery query, CancellationToken cancellationToken) + { + var organizationDocument = await _organizationRepository.GetAsync(o => o.Id == query.OrganizationId); + if (organizationDocument == null) + { + return null; + } + + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == query.OrganizationId); + var gallery = galleryDocument?.Gallery.Select(g => g.AsEntity()) ?? Enumerable.Empty(); + + var organization = organizationDocument.AsEntity(); + return new OrganizationGalleryDto(organization, gallery); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithUsersHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithUsersHandler.cs new file mode 100644 index 000000000..2de49aee4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizationWithUsersHandler.cs @@ -0,0 +1,38 @@ +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 System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + [ExcludeFromCodeCoverage] + public class GetOrganizationWithUsersHandler : IQueryHandler + { + private readonly IMongoRepository _organizationRepository; + + public GetOrganizationWithUsersHandler(IMongoRepository organizationRepository) + { + _organizationRepository = organizationRepository; + } + + public async Task HandleAsync(GetOrganizationWithUsers query, CancellationToken cancellationToken) + { + var organizationDocument = await _organizationRepository.GetAsync(o => o.Id == query.OrganizationId); + if (organizationDocument == null) + { + return null; + } + + var organization = organizationDocument.AsEntity(); + var users = organization.Users.ToList(); + + return new OrganizationUsersDto(organization, users); + } + } +} 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 deleted file mode 100644 index e12bede29..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetOrganizerOrganizationsHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -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; -using System.Diagnostics.CodeAnalysis; - -namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers -{ - [ExcludeFromCodeCoverage] - 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 roots = (await _repository.FindAsync(o => true)).Select(o =>o.AsEntity()); - var organizerOrganizations = new List(); - foreach (var root in roots) - { - var organizations = Organization.FindOrganizations(query.OrganizerId, root); - organizerOrganizations.AddRange(organizations.Select(o => new OrganizationDto(o, root.Id))); - } - - return organizerOrganizations; - } - } -} \ 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 index 05f940594..7d38ab7c1 100644 --- 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 @@ -3,6 +3,10 @@ using MiniSpace.Services.Organizations.Application.DTO; using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers @@ -18,6 +22,9 @@ public GetRootOrganizationsHandler(IMongoRepository } public async Task> HandleAsync(GetRootOrganizations query, CancellationToken cancellationToken) - => (await _repository.FindAsync(o => true)).Select(o =>o.AsDto(o.Id)); + { + var rootOrganizations = await _repository.FindAsync(o => o.ParentOrganizationId == null); + return rootOrganizations.Select(o => new OrganizationDto(o.AsEntity())); + } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserOrganizationsHandler.cs new file mode 100644 index 000000000..3abe9a833 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserOrganizationsHandler.cs @@ -0,0 +1,32 @@ +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; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserOrganizationsHandler : IQueryHandler> + { + private readonly IMongoRepository _repository; + + public GetUserOrganizationsHandler(IMongoRepository repository) + { + _repository = repository; + } + + public async Task> HandleAsync(GetUserOrganizations query, CancellationToken cancellationToken) + { + var filter = Builders.Filter.Eq(o => o.OwnerId, query.UserId); + var organizationDocuments = await _repository.Collection.Find(filter).ToListAsync(cancellationToken); + + return organizationDocuments.Select(org => new OrganizationDto(org.AsEntity())); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationGalleryMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationGalleryMongoRepository.cs new file mode 100644 index 000000000..836728e8b --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationGalleryMongoRepository.cs @@ -0,0 +1,107 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class OrganizationGalleryMongoRepository : IOrganizationGalleryRepository + { + private readonly IMongoRepository _galleryRepository; + + public OrganizationGalleryMongoRepository(IMongoRepository galleryRepository) + { + _galleryRepository = galleryRepository; + } + + public async Task GetImageAsync(Guid organizationId, Guid imageId) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + var imageDocument = galleryDocument?.Gallery.FirstOrDefault(i => i.ImageId == imageId); + return imageDocument?.AsEntity(); + } + + public async Task> GetGalleryAsync(Guid organizationId) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + return galleryDocument?.Gallery.Select(g => g.AsEntity()); + } + + public async Task AddImageAsync(Guid organizationId, GalleryImage image) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + if (galleryDocument != null) + { + var gallery = galleryDocument.Gallery.ToList(); + gallery.Add(image.AsDocument()); + galleryDocument.Gallery = gallery; + await _galleryRepository.UpdateAsync(galleryDocument); + } + else + { + galleryDocument = new OrganizationGalleryImageDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Gallery = new List { image.AsDocument() } + }; + await _galleryRepository.AddAsync(galleryDocument); + } + } + + public async Task UpdateImageAsync(Guid organizationId, GalleryImage image) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + if (galleryDocument != null) + { + var gallery = galleryDocument.Gallery.ToList(); + var imageDocument = gallery.FirstOrDefault(g => g.ImageId == image.ImageId); + if (imageDocument != null) + { + gallery.Remove(imageDocument); + gallery.Add(image.AsDocument()); + galleryDocument.Gallery = gallery; + await _galleryRepository.UpdateAsync(galleryDocument); + } + } + } + + public async Task DeleteImageAsync(Guid organizationId, Guid imageId) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + if (galleryDocument != null) + { + var gallery = galleryDocument.Gallery.ToList(); + var imageDocument = gallery.FirstOrDefault(g => g.ImageId == imageId); + if (imageDocument != null) + { + gallery.Remove(imageDocument); + galleryDocument.Gallery = gallery; + await _galleryRepository.UpdateAsync(galleryDocument); + } + } + } + + public async Task RemoveImageAsync(Guid organizationId, string mediaFileUrl) + { + var galleryDocument = await _galleryRepository.GetAsync(g => g.OrganizationId == organizationId); + if (galleryDocument != null) + { + var gallery = galleryDocument.Gallery.ToList(); + var imageDocument = gallery.FirstOrDefault(g => g.ImageUrl == mediaFileUrl); + if (imageDocument != null) + { + gallery.Remove(imageDocument); + galleryDocument.Gallery = gallery; + await _galleryRepository.UpdateAsync(galleryDocument); + } + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs new file mode 100644 index 000000000..20e2d5e96 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs @@ -0,0 +1,90 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class OrganizationMembersMongoRepository : IOrganizationMembersRepository + { + private readonly IMongoRepository _userRepository; + + public OrganizationMembersMongoRepository(IMongoRepository userRepository) + { + _userRepository = userRepository; + } + + public async Task GetMemberAsync(Guid organizationId, Guid memberId) + { + var userDocument = await _userRepository.GetAsync(u => u.OrganizationId == organizationId && u.Users.Any(m => m.UserId == memberId)); + return userDocument?.Users.FirstOrDefault(u => u.UserId == memberId)?.AsEntity(); + } + + public async Task> GetMembersAsync(Guid organizationId) + { + var userDocument = await _userRepository.GetAsync(u => u.OrganizationId == organizationId); + return userDocument?.Users.Select(u => u.AsEntity()); + } + + public async Task AddMemberAsync(Guid organizationId, User member) + { + var userDocument = await _userRepository.GetAsync(u => u.OrganizationId == organizationId); + if (userDocument != null) + { + var users = userDocument.Users.ToList(); + users.Add(member.AsDocument()); + userDocument.Users = users; + await _userRepository.UpdateAsync(userDocument); + } + else + { + userDocument = new OrganizationMembersDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Users = new List { member.AsDocument() } + }; + await _userRepository.AddAsync(userDocument); + } + } + + public async Task UpdateMemberAsync(Guid organizationId, User member) + { + var userDocument = await _userRepository.GetAsync(u => u.OrganizationId == organizationId); + if (userDocument != null) + { + var users = userDocument.Users.ToList(); + var existingMember = users.FirstOrDefault(u => u.UserId == member.Id); + if (existingMember != null) + { + users.Remove(existingMember); + users.Add(member.AsDocument()); + userDocument.Users = users; + await _userRepository.UpdateAsync(userDocument); + } + } + } + + public async Task DeleteMemberAsync(Guid organizationId, Guid memberId) + { + var userDocument = await _userRepository.GetAsync(u => u.OrganizationId == organizationId); + if (userDocument != null) + { + var users = userDocument.Users.ToList(); + var existingMember = users.FirstOrDefault(u => u.UserId == memberId); + if (existingMember != null) + { + users.Remove(existingMember); + userDocument.Users = users; + await _userRepository.UpdateAsync(userDocument); + } + } + } + } +} 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 index 506c1853f..7c1435c27 100644 --- 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 @@ -2,6 +2,10 @@ using MiniSpace.Services.Organizations.Core.Entities; using MiniSpace.Services.Organizations.Core.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories @@ -9,35 +13,106 @@ namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories [ExcludeFromCodeCoverage] public class OrganizationMongoRepository : IOrganizationRepository { - private readonly IMongoRepository _repository; + private readonly IMongoRepository _organizationRepository; + private readonly IMongoRepository _membersRepository; - public OrganizationMongoRepository(IMongoRepository repository) + public OrganizationMongoRepository( + IMongoRepository organizationRepository, + IMongoRepository membersRepository) { - _repository = repository; + _organizationRepository = organizationRepository; + _membersRepository = membersRepository; } - + public async Task GetAsync(Guid id) { - var organization = await _repository.GetAsync(o => o.Id == id); - + var organization = await _organizationRepository.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)); - + var memberDocuments = await _membersRepository.FindAsync(m => m.Users.Any(u => u.UserId == organizerId)); + var organizationIds = memberDocuments.Select(m => m.OrganizationId); + var organizations = await _organizationRepository.FindAsync(o => organizationIds.Contains(o.Id)); return organizations?.Select(o => o.AsEntity()); } public Task AddAsync(Organization organization) - => _repository.AddAsync(organization.AsDocument()); + => _organizationRepository.AddAsync(organization.AsDocument()); public Task UpdateAsync(Organization organization) - => _repository.UpdateAsync(organization.AsDocument()); + => _organizationRepository.UpdateAsync(organization.AsDocument()); public Task DeleteAsync(Guid id) - => _repository.DeleteAsync(id); - } -} \ No newline at end of file + => _organizationRepository.DeleteAsync(id); + + public async Task GetMemberAsync(Guid organizationId, Guid memberId) + { + var memberDocument = await _membersRepository.GetAsync(m => m.OrganizationId == organizationId); + var userDocument = memberDocument?.Users.FirstOrDefault(u => u.UserId == memberId); + return userDocument?.AsEntity(); + } + + public async Task> GetMembersAsync(Guid organizationId) + { + var memberDocument = await _membersRepository.GetAsync(m => m.OrganizationId == organizationId); + return memberDocument?.Users.Select(u => u.AsEntity()); + } + + public async Task AddMemberAsync(Guid organizationId, User member) + { + var memberDocument = await _membersRepository.GetAsync(m => m.OrganizationId == organizationId); + if (memberDocument != null) + { + var users = memberDocument.Users.ToList(); + users.Add(member.AsDocument()); + memberDocument.Users = users; + await _membersRepository.UpdateAsync(memberDocument); + } + else + { + memberDocument = new OrganizationMembersDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Users = new List { member.AsDocument() } + }; + await _membersRepository.AddAsync(memberDocument); + } + } + + public async Task UpdateMemberAsync(Guid organizationId, User member) + { + var memberDocument = await _membersRepository.GetAsync(m => m.OrganizationId == organizationId); + if (memberDocument != null) + { + var users = memberDocument.Users.ToList(); + var existingMember = users.FirstOrDefault(u => u.UserId == member.Id); + if (existingMember != null) + { + users.Remove(existingMember); + users.Add(member.AsDocument()); + memberDocument.Users = users; + await _membersRepository.UpdateAsync(memberDocument); + } + } + } + + public async Task DeleteMemberAsync(Guid organizationId, Guid memberId) + { + var memberDocument = await _membersRepository.GetAsync(m => m.OrganizationId == organizationId); + if (memberDocument != null) + { + var users = memberDocument.Users.ToList(); + var existingMember = users.FirstOrDefault(u => u.UserId == memberId); + if (existingMember != null) + { + users.Remove(existingMember); + memberDocument.Users = users; + await _membersRepository.UpdateAsync(memberDocument); + } + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationRolesMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationRolesMongoRepository.cs new file mode 100644 index 000000000..b653c25ca --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationRolesMongoRepository.cs @@ -0,0 +1,97 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class OrganizationRolesMongoRepository : IOrganizationRolesRepository + { + private readonly IMongoRepository _rolesRepository; + + public OrganizationRolesMongoRepository(IMongoRepository rolesRepository) + { + _rolesRepository = rolesRepository; + } + + public async Task GetRoleAsync(Guid organizationId, Guid roleId) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.OrganizationId == organizationId); + return rolesDocument?.Roles.FirstOrDefault(r => r.Id == roleId)?.AsEntity(); + } + + + public async Task> GetRolesAsync(Guid organizationId) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.OrganizationId == organizationId); + return rolesDocument?.Roles.Select(r => r.AsEntity()); + } + + public async Task GetRoleByNameAsync(Guid organizationId, string roleName) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.OrganizationId == organizationId && r.Roles.Any(role => role.Name == roleName)); + return rolesDocument?.Roles.FirstOrDefault(r => r.Name == roleName)?.AsEntity(); + } + + public async Task AddRoleAsync(Guid organizationId, Role role) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.OrganizationId == organizationId); + if (rolesDocument != null) + { + var roles = rolesDocument.Roles.ToList(); + roles.Add(RoleEntry.FromEntity(role)); + rolesDocument.Roles = roles; + await _rolesRepository.UpdateAsync(rolesDocument); + } + else + { + var newDocument = new OrganizationRolesDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Roles = new List { RoleEntry.FromEntity(role) } + }; + await _rolesRepository.AddAsync(newDocument); + } + } + + public async Task UpdateRoleAsync(Role role) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.OrganizationId == role.Id); + if (rolesDocument != null) + { + var roles = rolesDocument.Roles.ToList(); + var existingRole = roles.FirstOrDefault(r => r.Id == role.Id); + if (existingRole != null) + { + roles.Remove(existingRole); + roles.Add(role.AsDocument()); + rolesDocument.Roles = roles; + await _rolesRepository.UpdateAsync(rolesDocument); + } + } + } + + public async Task DeleteRoleAsync(Guid roleId) + { + var rolesDocument = await _rolesRepository.GetAsync(r => r.Roles.Any(role => role.Id == roleId)); + if (rolesDocument != null) + { + var roles = rolesDocument.Roles.ToList(); + var roleToDelete = roles.FirstOrDefault(r => r.Id == roleId); + if (roleToDelete != null) + { + roles.Remove(roleToDelete); + rolesDocument.Roles = roles; + await _rolesRepository.UpdateAsync(rolesDocument); + } + } + } + } +} 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 deleted file mode 100644 index 88f148351..000000000 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizerMongoRepository.cs +++ /dev/null @@ -1,30 +0,0 @@ -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; -using System.Diagnostics.CodeAnalysis; - -namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories -{ - [ExcludeFromCodeCoverage] - 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/Mongo/Repositories/UserInvitationsMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/UserInvitationsMongoRepository.cs new file mode 100644 index 000000000..ecca33727 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/UserInvitationsMongoRepository.cs @@ -0,0 +1,73 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class UserInvitationsMongoRepository : IUserInvitationsRepository + { + private readonly IMongoRepository _invitationRepository; + + public UserInvitationsMongoRepository(IMongoRepository invitationRepository) + { + _invitationRepository = invitationRepository; + } + + public async Task GetInvitationAsync(Guid organizationId, Guid userId) + { + var invitationDocument = await _invitationRepository.GetAsync(i => i.OrganizationId == organizationId && i.Invitations.Any(inv => inv.UserId == userId)); + return invitationDocument?.Invitations.FirstOrDefault(i => i.UserId == userId)?.AsEntity(); + } + + public async Task> GetInvitationsAsync(Guid organizationId) + { + var invitationDocument = await _invitationRepository.GetAsync(i => i.OrganizationId == organizationId); + return invitationDocument?.Invitations.Select(i => i.AsEntity()); + } + + public async Task AddInvitationAsync(Guid organizationId, Invitation invitation) + { + var invitationDocument = await _invitationRepository.GetAsync(i => i.OrganizationId == organizationId); + if (invitationDocument != null) + { + var invitations = invitationDocument.Invitations.ToList(); + invitations.Add(invitation.AsDocument()); + invitationDocument.Invitations = invitations; + await _invitationRepository.UpdateAsync(invitationDocument); + } + else + { + invitationDocument = new OrganizationInvitationDocument + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Invitations = new List { invitation.AsDocument() } + }; + await _invitationRepository.AddAsync(invitationDocument); + } + } + + public async Task DeleteInvitationAsync(Guid organizationId, Guid userId) + { + var invitationDocument = await _invitationRepository.GetAsync(i => i.OrganizationId == organizationId); + if (invitationDocument != null) + { + var invitations = invitationDocument.Invitations.ToList(); + var invitation = invitations.FirstOrDefault(i => i.UserId == userId); + if (invitation != null) + { + invitations.Remove(invitation); + invitationDocument.Invitations = invitations; + await _invitationRepository.UpdateAsync(invitationDocument); + } + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/.gitignore b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/.gitignore b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/.gitignore b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Reports/src/MiniSpace.Services.Reports.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/.gitignore b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json 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 2ee9a562a..5e55a750a 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs @@ -34,15 +34,25 @@ public static async Task Main(string[] args) .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) .Get>("students") .Get("students/{studentId}") + .Get("students/{studentId}/settings") + .Get("students/{studentId}/gallery") + .Get("students/{studentId}/visibility-settings") + .Get("students/{studentId}/events") + .Get("students/{studentId}/notifications") + .Put("students/{studentId}") - .Delete("students/{studentId}") - .Post("students", - afterDispatch: (cmd, ctx) => ctx.Response.Created($"students/{cmd.StudentId}")) + .Put("students/{studentId}/settings") .Put("students/{studentId}/state/{state}", afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) - .Get("students/{studentId}/events") - .Get("students/{studentId}/notifications") - .Post("students/{studentId}/notifications"))) + .Put("students/{studentId}/languages-and-interests") + + .Delete("students/{studentId}") + + .Post("students", + afterDispatch: (cmd, ctx) => ctx.Response.Created($"students/{cmd.StudentId}")) + .Post("students/{studentId}/notifications") + + )) .UseLogging() .Build() .RunAsync(); diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs index b48f13680..33c6dd68b 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentHandler.cs @@ -1,7 +1,9 @@ using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Dto; using MiniSpace.Services.Students.Application.Events; using MiniSpace.Services.Students.Application.Exceptions; using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; using MiniSpace.Services.Students.Core.Repositories; using System; using System.Linq; @@ -45,14 +47,20 @@ public async Task HandleAsync(UpdateStudent command, CancellationToken cancellat throw new UnauthorizedStudentAccessException(command.StudentId, identity.Id); } - student.Update(command.FirstName, command.LastName, command.ProfileImageUrl, command.Description, command.EmailNotifications, command.ContactEmail); - student.UpdateBannerUrl(command.BannerUrl); - student.UpdateGalleryOfImageUrls(command.GalleryOfImageUrls); - student.UpdateEducation(command.Education); - student.UpdateWorkPosition(command.WorkPosition); - student.UpdateCompany(command.Company); - student.UpdateLanguages(command.Languages); - student.UpdateInterests(command.Interests); + student.Update(command.FirstName, + command.LastName, + command.Description, + command.EmailNotifications, + command.ContactEmail, + command.PhoneNumber, + command.Country, + command.City, + command.DateOfBirth); + + student.UpdateEducation(command.Education.Select(e => new Education(e.InstitutionName, e.Degree, e.StartDate, e.EndDate, e.Description))); + student.UpdateWork(command.Work.Select(w => new Work(w.Company, w.Position, w.StartDate, w.EndDate, w.Description))); + student.UpdateLanguages(command.Languages.Select(l => (Language)Enum.Parse(typeof(Language), l))); + student.UpdateInterests(command.Interests.Select(i => (Interest)Enum.Parse(typeof(Interest), i))); if (command.EnableTwoFactor) { @@ -69,15 +77,29 @@ public async Task HandleAsync(UpdateStudent command, CancellationToken cancellat var studentUpdatedEvent = new StudentUpdated( student.Id, student.FullName, - student.ProfileImageUrl, - student.BannerUrl, - student.GalleryOfImageUrls, - student.Education, - student.WorkPosition, - student.Company, - student.Languages, - student.Interests, - student.ContactEmail + student.Description, + student.Education.Select(e => new EducationDto + { + InstitutionName = e.InstitutionName, + Degree = e.Degree, + StartDate = e.StartDate, + EndDate = e.EndDate, + Description = e.Description + }).ToList(), + student.Work.Select(w => new WorkDto + { + Company = w.Company, + Position = w.Position, + StartDate = w.StartDate, + EndDate = w.EndDate, + Description = w.Description + }).ToList(), + student.Languages.Select(l => l.ToString()).ToList(), + student.Interests.Select(i => i.ToString()).ToList(), + student.ContactEmail, + student.Country, + student.City, + student.DateOfBirth ); await _messageBroker.PublishAsync(studentUpdatedEvent); diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentLanguagesAndInterestsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentLanguagesAndInterestsHandler.cs new file mode 100644 index 000000000..914af9037 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateStudentLanguagesAndInterestsHandler.cs @@ -0,0 +1,86 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Events; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class UpdateStudentLanguagesAndInterestsHandler : ICommandHandler + { + private readonly IStudentRepository _studentRepository; + private readonly IAppContext _appContext; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UpdateStudentLanguagesAndInterestsHandler(IStudentRepository studentRepository, IAppContext appContext, + IEventMapper eventMapper, IMessageBroker messageBroker) + { + _studentRepository = studentRepository; + _appContext = appContext; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateStudentLanguagesAndInterests command, CancellationToken cancellationToken = default) + { + // Log the command received + var commandJson = JsonSerializer.Serialize(command); + Console.WriteLine($"Received UpdateStudentLanguagesAndInterests command: {commandJson}"); + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student is null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var identity = _appContext.Identity; + if (identity.IsAuthenticated && identity.Id != student.Id && !identity.IsAdmin) + { + throw new UnauthorizedStudentAccessException(command.StudentId, identity.Id); + } + + student.UpdateLanguages(command.Languages.Select(l => (Language)Enum.Parse(typeof(Language), l))); + student.UpdateInterests(command.Interests.Select(i => (Interest)Enum.Parse(typeof(Interest), i))); + + await _studentRepository.UpdateAsync(student); + + var studentUpdatedEvent = new StudentUpdated( + student.Id, + student.FullName, + student.Description, + student.Education.Select(e => new EducationDto + { + InstitutionName = e.InstitutionName, + Degree = e.Degree, + StartDate = e.StartDate, + EndDate = e.EndDate, + Description = e.Description + }).ToList(), + student.Work.Select(w => new WorkDto + { + Company = w.Company, + Position = w.Position, + StartDate = w.StartDate, + EndDate = w.EndDate, + Description = w.Description + }).ToList(), + student.Languages.Select(l => l.ToString()).ToList(), + student.Interests.Select(i => i.ToString()).ToList(), + student.ContactEmail, + student.Country, + student.City, + student.DateOfBirth + ); + + await _messageBroker.PublishAsync(studentUpdatedEvent); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserNotificationPreferencesHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserNotificationPreferencesHandler.cs index e6a76682d..028d0793d 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserNotificationPreferencesHandler.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserNotificationPreferencesHandler.cs @@ -11,15 +11,16 @@ namespace MiniSpace.Services.Students.Application.Commands.Handlers public class UpdateUserNotificationPreferencesHandler : ICommandHandler { private readonly IUserNotificationPreferencesRepository _userNotificationPreferencesRepository; + private readonly IStudentRepository _studentRepository; - public UpdateUserNotificationPreferencesHandler(IUserNotificationPreferencesRepository userNotificationPreferencesRepository) + public UpdateUserNotificationPreferencesHandler(IUserNotificationPreferencesRepository userNotificationPreferencesRepository, IStudentRepository studentRepository) { _userNotificationPreferencesRepository = userNotificationPreferencesRepository; + _studentRepository = studentRepository; } public async Task HandleAsync(UpdateUserNotificationPreferences command, CancellationToken cancellationToken = default) { - // Log the command received var commandJson = JsonSerializer.Serialize(command); Console.WriteLine($"Received UpdateUserNotificationPreferences command: {commandJson}"); @@ -35,6 +36,13 @@ public async Task HandleAsync(UpdateUserNotificationPreferences command, Cancell ); await _userNotificationPreferencesRepository.UpdateNotificationPreferencesAsync(command.StudentId, notificationPreferences); + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student != null) + { + student.SetEmailNotifications(command.EmailNotifications); + await _studentRepository.UpdateAsync(student); + } } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserSettingsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserSettingsHandler.cs new file mode 100644 index 000000000..99b139512 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UpdateUserSettingsHandler.cs @@ -0,0 +1,74 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class UpdateUserSettingsHandler : ICommandHandler + { + private readonly IUserSettingsRepository _userSettingsRepository; + private readonly IStudentRepository _studentRepository; + private readonly IAppContext _appContext; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UpdateUserSettingsHandler( + IUserSettingsRepository userSettingsRepository, + IStudentRepository studentRepository, + IAppContext appContext, + IEventMapper eventMapper, + IMessageBroker messageBroker) + { + _userSettingsRepository = userSettingsRepository; + _studentRepository = studentRepository; + _appContext = appContext; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UpdateUserSettings command, CancellationToken cancellationToken = default) + { + + var student = await _studentRepository.GetAsync(command.StudentId); + if (student == null) + { + throw new StudentNotFoundException(command.StudentId); + } + + var userSettings = await _userSettingsRepository.GetUserSettingsAsync(command.StudentId); + if (userSettings == null) + { + throw new UserSettingsNotFoundException(command.StudentId); + } + + + var availableSettings = new UserAvailableSettings( + Enum.Parse(command.CreatedAtVisibility, true), + Enum.Parse(command.DateOfBirthVisibility, true), + Enum.Parse(command.InterestedInEventsVisibility, true), + Enum.Parse(command.SignedUpEventsVisibility, true), + Enum.Parse(command.EducationVisibility, true), + Enum.Parse(command.WorkPositionVisibility, true), + Enum.Parse(command.LanguagesVisibility, true), + Enum.Parse(command.InterestsVisibility, true), + Enum.Parse(command.ContactEmailVisibility, true), + Enum.Parse(command.PhoneNumberVisibility, true), + Enum.Parse(command.FrontendVersion, true), + Enum.Parse(command.PreferredLanguage, true) + ); + + userSettings.UpdateSettings(availableSettings); + await _userSettingsRepository.UpdateUserSettingsAsync(userSettings); + + var events = _eventMapper.MapAll(userSettings.Events); + await _messageBroker.PublishAsync(events); + + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs index d76d0a8a5..ca5f6c45c 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs @@ -1,4 +1,5 @@ using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Dto; using System; using System.Collections.Generic; using System.Linq; @@ -10,43 +11,44 @@ public class UpdateStudent : ICommand public Guid StudentId { get; } public string FirstName { get; } public string LastName { get; } - public string ProfileImageUrl { get; } - public string Description { get; } + public string? Description { get; } public bool EmailNotifications { get; } - public string? BannerUrl { get; } - public IEnumerable GalleryOfImageUrls { get; } - public string Education { get; } - public string WorkPosition { get; } - public string Company { get; } + public IEnumerable Education { get; } + public IEnumerable Work { get; } public IEnumerable Languages { get; } public IEnumerable Interests { get; } public bool EnableTwoFactor { get; } public bool DisableTwoFactor { get; } public string TwoFactorSecret { get; } public string? ContactEmail { get; } + public string PhoneNumber { get; } + public string Country { get; } + public string City { get; } + public DateTime? DateOfBirth { get; } - public UpdateStudent(Guid studentId, string firstName, string lastName, string profileImageUrl, string description, bool emailNotifications, - string? bannerUrl, IEnumerable galleryOfImageUrls, string education, string workPosition, - string company, IEnumerable languages, IEnumerable interests, - bool enableTwoFactor, bool disableTwoFactor, string twoFactorSecret, string? contactEmail) + public UpdateStudent(Guid studentId, string firstName, string lastName, string? description, bool emailNotifications, + IEnumerable education, IEnumerable work, + IEnumerable languages, IEnumerable interests, + bool enableTwoFactor, bool disableTwoFactor, string twoFactorSecret, string? contactEmail, + string phoneNumber, string country, string city, DateTime? dateOfBirth) { StudentId = studentId; FirstName = firstName; LastName = lastName; - ProfileImageUrl = profileImageUrl; Description = description; EmailNotifications = emailNotifications; - BannerUrl = bannerUrl; - GalleryOfImageUrls = galleryOfImageUrls ?? Enumerable.Empty(); - Education = education; - WorkPosition = workPosition; - Company = company; + Education = education ?? Enumerable.Empty(); + Work = work ?? Enumerable.Empty(); Languages = languages ?? Enumerable.Empty(); Interests = interests ?? Enumerable.Empty(); EnableTwoFactor = enableTwoFactor; DisableTwoFactor = disableTwoFactor; TwoFactorSecret = twoFactorSecret; ContactEmail = contactEmail; + PhoneNumber = phoneNumber; + Country = country; + City = city; + DateOfBirth = dateOfBirth; } - } + } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudentLanguagesAndInterests.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudentLanguagesAndInterests.cs new file mode 100644 index 000000000..012095465 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudentLanguagesAndInterests.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UpdateStudentLanguagesAndInterests : ICommand + { + public Guid StudentId { get; } + public IEnumerable Languages { get; } + public IEnumerable Interests { get; } + + public UpdateStudentLanguagesAndInterests(Guid studentId, IEnumerable languages, IEnumerable interests) + { + StudentId = studentId; + Languages = languages ?? throw new ArgumentNullException(nameof(languages)); + Interests = interests ?? throw new ArgumentNullException(nameof(interests)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserGallery.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserGallery.cs new file mode 100644 index 000000000..5e3a49d6c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserGallery.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Dto; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UpdateUserGallery : ICommand + { + public Guid UserId { get; } + public IEnumerable GalleryOfImages { get; } + + public UpdateUserGallery(Guid userId, IEnumerable galleryOfImages) + { + UserId = userId; + GalleryOfImages = galleryOfImages ?? throw new ArgumentNullException(nameof(galleryOfImages)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserNotificationPreferences.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserNotificationPreferences.cs index 4f8ea0451..ade0c770f 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserNotificationPreferences.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserNotificationPreferences.cs @@ -5,30 +5,16 @@ namespace MiniSpace.Services.Students.Application.Commands { public class UpdateUserNotificationPreferences : ICommand { - public Guid StudentId { get; } - public bool AccountChanges { get; } - public bool SystemLogin { get; } - public bool NewEvent { get; } - public bool InterestBasedEvents { get; } - public bool EventNotifications { get; } - public bool CommentsNotifications { get; } - public bool PostsNotifications { get; } - public bool FriendsNotifications { get; } - - public UpdateUserNotificationPreferences(Guid studentId, bool accountChanges, bool systemLogin, bool newEvent, - bool interestBasedEvents, bool eventNotifications, - bool commentsNotifications, bool postsNotifications, - bool friendsNotifications) - { - StudentId = studentId; - AccountChanges = accountChanges; - SystemLogin = systemLogin; - NewEvent = newEvent; - InterestBasedEvents = interestBasedEvents; - EventNotifications = eventNotifications; - CommentsNotifications = commentsNotifications; - PostsNotifications = postsNotifications; - FriendsNotifications = friendsNotifications; - } + public Guid StudentId { get; set; } + public bool EmailNotifications { get; set; } + public bool AccountChanges { get; set; } + public bool SystemLogin { get; set; } + public bool NewEvent { get; set; } + public bool InterestBasedEvents { get; set; } + public bool EventNotifications { get; set; } + public bool CommentsNotifications { get; set; } + public bool PostsNotifications { get; set; } + public bool FriendsNotifications { get; set; } } + } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserSettings.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserSettings.cs new file mode 100644 index 000000000..625acc59d --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateUserSettings.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UpdateUserSettings : ICommand + { + public Guid StudentId { get; set;} + public string CreatedAtVisibility { get; set; } + public string DateOfBirthVisibility { get; set; } + public string InterestedInEventsVisibility { get; set; } + public string SignedUpEventsVisibility { get; set; } + public string EducationVisibility { get; set; } + public string WorkPositionVisibility { get; set; } + public string LanguagesVisibility { get; set; } + public string InterestsVisibility { get; set; } + public string ContactEmailVisibility { get; set; } + public string PhoneNumberVisibility { get; set; } + public string PreferredLanguage { get; set; } + public string FrontendVersion { get; set; } + + public UpdateUserSettings(Guid studentId, string createdAtVisibility, string dateOfBirthVisibility, + string interestedInEventsVisibility, string signedUpEventsVisibility, + string educationVisibility, string workPositionVisibility, + string languagesVisibility, string interestsVisibility, + string contactEmailVisibility, string phoneNumberVisibility, + string preferredLanguage, string frontendVersion) + { + StudentId = studentId; + CreatedAtVisibility = createdAtVisibility; + DateOfBirthVisibility = dateOfBirthVisibility; + InterestedInEventsVisibility = interestedInEventsVisibility; + SignedUpEventsVisibility = signedUpEventsVisibility; + EducationVisibility = educationVisibility; + WorkPositionVisibility = workPositionVisibility; + LanguagesVisibility = languagesVisibility; + InterestsVisibility = interestsVisibility; + ContactEmailVisibility = contactEmailVisibility; + PhoneNumberVisibility = phoneNumberVisibility; + PreferredLanguage = preferredLanguage; + FrontendVersion = frontendVersion; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/AvailableSettingsDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/AvailableSettingsDto.cs new file mode 100644 index 000000000..ca73bb2d0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/AvailableSettingsDto.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class AvailableSettingsDto + { + public string CreatedAtVisibility { get; set; } + public string DateOfBirthVisibility { get; set; } + public string InterestedInEventsVisibility { get; set; } + public string SignedUpEventsVisibility { get; set; } + public string EducationVisibility { get; set; } + public string WorkPositionVisibility { get; set; } + public string LanguagesVisibility { get; set; } + public string InterestsVisibility { get; set; } + public string ContactEmailVisibility { get; set; } + public string PhoneNumberVisibility { get; set; } + public string PreferredLanguage { get; set; } + public string FrontendVersion { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/EducationDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/EducationDto.cs new file mode 100644 index 000000000..ef7b07c5c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/EducationDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class EducationDto + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } + +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/GalleryImageDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/GalleryImageDto.cs new file mode 100644 index 000000000..14570f592 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/GalleryImageDto.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class GalleryImageDto + { + public Guid ImageId { get; set; } + public string ImageUrl { get; set; } + public DateTime DateAdded { get; set; } + + public GalleryImageDto(Guid imageId, string imageUrl, DateTime dateAdded) + { + ImageId = imageId; + ImageUrl = imageUrl ?? throw new ArgumentNullException(nameof(imageUrl)); + DateAdded = dateAdded; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/InterestDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/InterestDto.cs new file mode 100644 index 000000000..11bf3cae0 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/InterestDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class InterestDto + { + public string Name { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/NotificationPreferencesDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/NotificationPreferencesDto.cs index e8e3b9151..e0f89a6d1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/NotificationPreferencesDto.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/NotificationPreferencesDto.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Students.Application.Dto { + [ExcludeFromCodeCoverage] public class NotificationPreferencesDto { public Guid StudentId { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs index 9eeebb56b..7a2edfac1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Students.Core.Entities; namespace MiniSpace.Services.Students.Application.Dto { @@ -11,27 +12,25 @@ public class StudentDto public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public int NumberOfFriends { get; set; } public string ProfileImageUrl { get; set; } 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 string Education { get; set; } - public string WorkPosition { get; set; } - public string Company { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } public IEnumerable Languages { get; set; } public IEnumerable Interests { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } public IEnumerable InterestedInEvents { get; set; } public IEnumerable SignedUpEvents { get; set; } - public string BannerUrl { get; set; } - public IEnumerable GalleryOfImageUrls { get; set; } - public string ContactEmail { get; set; } - public NotificationPreferencesDto NotificationPreferences { get; set; } + public string Country { get; set; } + public string City { get; set; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithGalleryImagesDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithGalleryImagesDto.cs new file mode 100644 index 000000000..a1f016f99 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithGalleryImagesDto.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class StudentWithGalleryImagesDto + { + public StudentDto Student { get; set; } + public List GalleryImages { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithVisibilitySettingsDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithVisibilitySettingsDto.cs new file mode 100644 index 000000000..108bf3c37 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentWithVisibilitySettingsDto.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class StudentWithVisibilitySettingsDto + { + public StudentDto Student { get; set; } + public AvailableSettingsDto VisibilitySettings { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserGalleryDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserGalleryDto.cs new file mode 100644 index 000000000..6a65af617 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserGalleryDto.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class UserGalleryDto + { + public Guid UserId { get; set; } + public IEnumerable GalleryOfImages { get; set; } + + public UserGalleryDto(Guid userId, IEnumerable galleryOfImages) + { + UserId = userId; + GalleryOfImages = galleryOfImages ?? throw new ArgumentNullException(nameof(galleryOfImages)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserSettingsDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserSettingsDto.cs new file mode 100644 index 000000000..974f3d175 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserSettingsDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class UserSettingsDto + { + public Guid StudentId { get; set; } + public string CreatedAtVisibility { get; set; } + public string DateOfBirthVisibility { get; set; } + public string InterestedInEventsVisibility { get; set; } + public string SignedUpEventsVisibility { get; set; } + public string EducationVisibility { get; set; } + public string WorkPositionVisibility { get; set; } + public string LanguagesVisibility { get; set; } + public string InterestsVisibility { get; set; } + public string ContactEmailVisibility { get; set; } + public string PhoneNumberVisibility { get; set; } + public string PreferredLanguage { get; set; } + public string FrontendVersion { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/WorkDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/WorkDto.cs new file mode 100644 index 000000000..a7a7114d4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/WorkDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class WorkDto + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/EmailVerifiedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/EmailVerifiedHandler.cs new file mode 100644 index 000000000..abc18085a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/EmailVerifiedHandler.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Students.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class EmailVerifiedHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly ILogger _logger; + + public EmailVerifiedHandler(IStudentRepository studentRepository, ILogger logger) + { + _studentRepository = studentRepository; + _logger = logger; + } + + public async Task HandleAsync(EmailVerified @event, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(@event.UserId); + if (student == null) + { + _logger.LogWarning($"Student with ID: {@event.UserId} not found."); + return; + } + + student.VerifyEmail(@event.Email, @event.VerifiedAt); + await _studentRepository.UpdateAsync(student); + + _logger.LogInformation($"Email verified for student with ID: {@event.UserId}."); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs index 5c68f920e..4d15c6180 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -1,6 +1,8 @@ using Convey.CQRS.Events; using MiniSpace.Services.Students.Core.Repositories; +using System; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -9,58 +11,76 @@ namespace MiniSpace.Services.Students.Application.Events.External.Handlers public class MediaFileDeletedHandler : IEventHandler { private readonly IStudentRepository _studentRepository; + private readonly IUserGalleryRepository _userGalleryRepository; - public MediaFileDeletedHandler(IStudentRepository studentRepository) + public MediaFileDeletedHandler(IStudentRepository studentRepository, IUserGalleryRepository userGalleryRepository) { _studentRepository = studentRepository; + _userGalleryRepository = userGalleryRepository; } public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) { Console.WriteLine($"Received MediaFileDeleted event: {@event.MediaFileUrl}"); - if (@event.Source.ToLowerInvariant() != "studentprofileimage") + // Fetch the student data + var student = await _studentRepository.GetAsync(@event.UploaderId); + if (student == null) { - Console.WriteLine("Event source is not 'studentprofileimage', ignoring the event."); + Console.WriteLine($"Student with ID {@event.UploaderId} not found."); return; } - var student = await _studentRepository.GetAsync(@event.SourceId); - if (student != null) - { - bool updated = false; + // Fetch the user gallery using the student's ID + var userGallery = await _userGalleryRepository.GetAsync(student.Id); + Console.WriteLine($"Fetched student and user gallery data. {JsonSerializer.Serialize(userGallery, new JsonSerializerOptions { WriteIndented = true })}"); - // Check and remove profile image - if (student.ProfileImageUrl == @event.MediaFileUrl) - { - student.RemoveProfileImage(); - updated = true; - Console.WriteLine("Removed profile image."); - } + bool studentUpdated = false; + bool galleryUpdated = false; - // Check and remove banner image - if (student.BannerUrl == @event.MediaFileUrl) - { - student.RemoveBannerImage(); - updated = true; - Console.WriteLine("Removed banner image."); - } + // Handle profile image deletion + if (@event.Source.ToLowerInvariant() == "studentprofileimage" && student.ProfileImageUrl == @event.MediaFileUrl) + { + student.RemoveProfileImage(); + studentUpdated = true; + Console.WriteLine("Removed profile image."); + } + + // Handle banner image deletion + if (@event.Source.ToLowerInvariant() == "studentbannerimage" && student.BannerUrl == @event.MediaFileUrl) + { + student.RemoveBannerImage(); + studentUpdated = true; + Console.WriteLine("Removed banner image."); + } - // Check and remove gallery images - if (student.GalleryOfImageUrls.Contains(@event.MediaFileUrl)) + // Handle gallery image deletion + if (userGallery != null) + { + Console.WriteLine("User gallery is not null"); + var galleryImage = userGallery.GalleryOfImages.FirstOrDefault(img => img.ImageUrl == @event.MediaFileUrl); + if (galleryImage != null) { - student.RemoveGalleryImage(@event.MediaFileUrl); - updated = true; + Console.WriteLine("Gallery image found is not null"); + userGallery.RemoveGalleryImage(galleryImage.ImageId); + galleryUpdated = true; Console.WriteLine("Removed gallery image."); } + } - if (updated) - { - await _studentRepository.UpdateAsync(student); - Console.WriteLine("Updated student repository."); - } + // Update the student repository if necessary + if (studentUpdated) + { + await _studentRepository.UpdateAsync(student); + Console.WriteLine("Updated student repository."); } - } + // Update the user gallery repository if necessary + if (galleryUpdated) + { + await _userGalleryRepository.UpdateAsync(userGallery); + Console.WriteLine("Updated user gallery repository."); + } + } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs deleted file mode 100644 index c39946f5f..000000000 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsGrantedHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Students.Application.Exceptions; -using MiniSpace.Services.Students.Application.Services; -using MiniSpace.Services.Students.Core.Repositories; - -namespace MiniSpace.Services.Students.Application.Events.External.Handlers -{ - public class OrganizerRightsGrantedHandler : IEventHandler - { - private readonly IStudentRepository _studentRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - - public OrganizerRightsGrantedHandler(IStudentRepository studentRepository, - IEventMapper eventMapper, IMessageBroker messageBroker) - { - _studentRepository = studentRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(OrganizerRightsGranted @event, CancellationToken cancellationToken) - { - var student = await _studentRepository.GetAsync(@event.UserId); - if (student is null) - { - throw new StudentNotFoundException(@event.UserId); - } - - student.GrantOrganizerRights(); - await _studentRepository.UpdateAsync(student); - - var events = _eventMapper.MapAll(student.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - } -} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs deleted file mode 100644 index f4dffa3a0..000000000 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/OrganizerRightsRevokedHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Students.Application.Exceptions; -using MiniSpace.Services.Students.Application.Services; -using MiniSpace.Services.Students.Core.Repositories; - -namespace MiniSpace.Services.Students.Application.Events.External.Handlers -{ - public class OrganizerRightsRevokedHandler : IEventHandler - { - private readonly IStudentRepository _studentRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - - public OrganizerRightsRevokedHandler(IStudentRepository studentRepository, - IEventMapper eventMapper, IMessageBroker messageBroker) - { - _studentRepository = studentRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(OrganizerRightsRevoked @event, CancellationToken cancellationToken) - { - var student = await _studentRepository.GetAsync(@event.UserId); - if (student is null) - { - throw new StudentNotFoundException(@event.UserId); - } - - student.RevokeOrganizerRights(); - await _studentRepository.UpdateAsync(student); - - var events = _eventMapper.MapAll(student.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - } -} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs index 168c4d734..3577c8026 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/SignedUpHandler.cs @@ -6,24 +6,28 @@ using MiniSpace.Services.Students.Core.Repositories; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; namespace MiniSpace.Services.Students.Application.Events.External.Handlers { public class SignedUpHandler : IEventHandler { - private const string RequiredRole = "user"; + private const string RequiredRole = "User"; private readonly IStudentRepository _studentRepository; private readonly IDateTimeProvider _dateTimeProvider; private readonly ILogger _logger; private readonly IUserNotificationPreferencesRepository _notificationPreferencesRepository; + private readonly IUserSettingsRepository _userSettingsRepository; public SignedUpHandler(IStudentRepository studentRepository, IDateTimeProvider dateTimeProvider, - ILogger logger, IUserNotificationPreferencesRepository notificationPreferencesRepository) + ILogger logger, IUserNotificationPreferencesRepository notificationPreferencesRepository, + IUserSettingsRepository userSettingsRepository) { _studentRepository = studentRepository; _dateTimeProvider = dateTimeProvider; _logger = logger; _notificationPreferencesRepository = notificationPreferencesRepository; + _userSettingsRepository = userSettingsRepository; } public async Task HandleAsync(SignedUp @event, CancellationToken cancellationToken = default) @@ -39,14 +43,43 @@ public async Task HandleAsync(SignedUp @event, CancellationToken cancellationTok throw new StudentAlreadyCreatedException(student.Id); } - var newStudent = new Student(@event.UserId, @event.FirstName, @event.LastName, - @event.Email, _dateTimeProvider.Now); + var newStudent = new Student( + @event.UserId, + @event.Email, + _dateTimeProvider.Now, + @event.FirstName, + @event.LastName, + string.Empty, // ProfileImageUrl + string.Empty, // Description + null, // DateOfBirth + false, // EmailNotifications + false, // IsBanned + State.Unverified, // State + new List(), // InterestedInEvents + new List(), // SignedUpEvents + string.Empty, // BannerUrl + new List(), // Education + new List(), // Work + new List(), // Languages + new List(), // Interests + false, // IsTwoFactorEnabled + string.Empty, // TwoFactorSecret + string.Empty, // ContactEmail + string.Empty, // PhoneNumber, + string.Empty, // Country + string.Empty // City + ); + await _studentRepository.AddAsync(newStudent); var defaultPreferences = new NotificationPreferences(); await _notificationPreferencesRepository.UpdateNotificationPreferencesAsync(newStudent.Id, defaultPreferences); - _logger.LogInformation($"New student created with ID: {@event.UserId} and default notification preferences set."); + var defaultAvailableSettings = new UserAvailableSettings(); + var userSettings = new UserSettings(newStudent.Id, defaultAvailableSettings); + await _userSettingsRepository.AddUserSettingsAsync(userSettings); + + _logger.LogInformation($"New student created with ID: {@event.UserId}, default notification preferences set, and default user settings initialized."); } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentImageUploadedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentImageUploadedHandler.cs index 1b1aac931..0082e5442 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentImageUploadedHandler.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/StudentImageUploadedHandler.cs @@ -11,10 +11,12 @@ namespace MiniSpace.Services.Students.Application.Events.External.Handlers public class StudentImageUploadedHandler : IEventHandler { private readonly IStudentRepository _studentRepository; + private readonly IUserGalleryRepository _userGalleryRepository; - public StudentImageUploadedHandler(IStudentRepository studentRepository) + public StudentImageUploadedHandler(IStudentRepository studentRepository, IUserGalleryRepository userGalleryRepository) { _studentRepository = studentRepository; + _userGalleryRepository = userGalleryRepository; } public async Task HandleAsync(StudentImageUploaded @event, CancellationToken cancellationToken) @@ -29,13 +31,23 @@ public async Task HandleAsync(StudentImageUploaded @event, CancellationToken can { case nameof(ContextType.StudentProfileImage): student.UpdateProfileImageUrl(@event.ImageUrl); + Console.WriteLine("Updated profile image URL."); break; + case nameof(ContextType.StudentBannerImage): student.UpdateBannerUrl(@event.ImageUrl); break; + case nameof(ContextType.StudentGalleryImage): - student.AddGalleryImageUrl(@event.ImageUrl); + var userGallery = await _userGalleryRepository.GetAsync(@event.StudentId); + if (userGallery == null) + { + userGallery = new UserGallery(@event.StudentId); + } + userGallery.AddGalleryImage(Guid.NewGuid(), @event.ImageUrl); + await _userGalleryRepository.UpdateAsync(userGallery); break; + default: throw new InvalidContextTypeException(@event.ImageType); } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationDisabledHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationDisabledHandler.cs new file mode 100644 index 000000000..0e9d1a1e5 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationDisabledHandler.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Students.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class TwoFactorAuthenticationDisabledHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly ILogger _logger; + + public TwoFactorAuthenticationDisabledHandler(IStudentRepository studentRepository, ILogger logger) + { + _studentRepository = studentRepository; + _logger = logger; + } + + public async Task HandleAsync(TwoFactorAuthenticationDisabled @event, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(@event.UserId); + if (student == null) + { + _logger.LogWarning($"Student with ID: {@event.UserId} not found."); + return; + } + + student.DisableTwoFactorAuthentication(); + await _studentRepository.UpdateAsync(student); + + _logger.LogInformation($"Two-factor authentication disabled for student with ID: {@event.UserId}."); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationEnabledHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationEnabledHandler.cs new file mode 100644 index 000000000..83ca70999 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/TwoFactorAuthenticationEnabledHandler.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Students.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class TwoFactorAuthenticationEnabledHandler : IEventHandler + { + private readonly IStudentRepository _studentRepository; + private readonly ILogger _logger; + + public TwoFactorAuthenticationEnabledHandler(IStudentRepository studentRepository, ILogger logger) + { + _studentRepository = studentRepository; + _logger = logger; + } + + public async Task HandleAsync(TwoFactorAuthenticationEnabled @event, CancellationToken cancellationToken = default) + { + var student = await _studentRepository.GetAsync(@event.UserId); + if (student == null) + { + _logger.LogWarning($"Student with ID: {@event.UserId} not found."); + return; + } + + student.EnableTwoFactorAuthentication(@event.Secret); + await _studentRepository.UpdateAsync(student); + + _logger.LogInformation($"Two-factor authentication enabled for student with ID: {@event.UserId}."); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs index c6a00d89c..7de97b572 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs @@ -1,5 +1,6 @@ using Convey.CQRS.Events; using Convey.MessageBrokers; +using System; namespace MiniSpace.Services.Students.Application.Events.External { @@ -9,12 +10,14 @@ public class MediaFileDeleted : IEvent public string MediaFileUrl { get; } public Guid SourceId { get; } public string Source { get; } + public Guid UploaderId { get; } - public MediaFileDeleted(string mediaFileUrl, Guid sourceId, string source) + public MediaFileDeleted(string mediaFileUrl, Guid sourceId, string source, Guid uploaderId) { MediaFileUrl = mediaFileUrl; SourceId = sourceId; Source = source; + UploaderId = uploaderId; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentImageUploaded.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentImageUploaded.cs index 361ef0d1c..1e2940553 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentImageUploaded.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/StudentImageUploaded.cs @@ -10,12 +10,14 @@ public class StudentImageUploaded : IEvent public Guid StudentId { get; } public string ImageUrl { get; } public string ImageType { get; } + public DateTime UploadDate { get; } - public StudentImageUploaded(Guid studentId, string imageUrl, string imageType) + public StudentImageUploaded(Guid studentId, string imageUrl, string imageType, DateTime uploadDate) { StudentId = studentId; ImageUrl = imageUrl; ImageType = imageType; + UploadDate = uploadDate; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationDisabled.cs similarity index 65% rename from MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs rename to MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationDisabled.cs index eb0f57953..c15c01df1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsRevoked.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationDisabled.cs @@ -1,13 +1,15 @@ +using System; using Convey.CQRS.Events; using Convey.MessageBrokers; namespace MiniSpace.Services.Students.Application.Events.External { [Message("identity")] - public class OrganizerRightsRevoked : IEvent + public class TwoFactorAuthenticationDisabled : IEvent { public Guid UserId { get; } - public OrganizerRightsRevoked(Guid userId) + + public TwoFactorAuthenticationDisabled(Guid userId) { UserId = userId; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationEnabled.cs similarity index 53% rename from MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs rename to MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationEnabled.cs index e62aaecdb..02527b3fb 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/OrganizerRightsGranted.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/TwoFactorAuthenticationEnabled.cs @@ -1,15 +1,19 @@ +using System; using Convey.CQRS.Events; using Convey.MessageBrokers; namespace MiniSpace.Services.Students.Application.Events.External { [Message("identity")] - public class OrganizerRightsGranted : IEvent + public class TwoFactorAuthenticationEnabled : IEvent { public Guid UserId { get; } - public OrganizerRightsGranted(Guid userId) + public string Secret { get; } + + public TwoFactorAuthenticationEnabled(Guid userId, string secret) { UserId = userId; + Secret = secret; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs index 56a05047c..14674d8d4 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs @@ -1,4 +1,5 @@ using Convey.CQRS.Events; +using MiniSpace.Services.Students.Application.Dto; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -10,32 +11,32 @@ public class StudentUpdated : IEvent { public Guid StudentId { get; } public string FullName { get; } - public string ProfileImageUrl { get; } - public string BannerUrl { get; } - public IEnumerable GalleryOfImageUrls { get; } - public string Education { get; } - public string WorkPosition { get; } - public string Company { get; } + public string Description { get; } + public IEnumerable Education { get; } + public IEnumerable Work { get; } public IEnumerable Languages { get; } public IEnumerable Interests { get; } - public string ContactEmail { get; } // New property + public string ContactEmail { get; } + public string Country { get; } + public string City { get; } + public DateTime? DateOfBirth { get; } - public StudentUpdated(Guid studentId, string fullName, string profileImageUrl, string bannerUrl, - IEnumerable galleryOfImageUrls, string education, string workPosition, - string company, IEnumerable languages, IEnumerable interests, - string contactEmail) + public StudentUpdated(Guid studentId, string fullName, string description, + IEnumerable education, IEnumerable work, + IEnumerable languages, IEnumerable interests, + string contactEmail, string country, string city, DateTime? dateOfBirth) { StudentId = studentId; FullName = fullName; - ProfileImageUrl = profileImageUrl; - BannerUrl = bannerUrl; - GalleryOfImageUrls = galleryOfImageUrls; + Description = description; Education = education; - WorkPosition = workPosition; - Company = company; + Work = work; Languages = languages; Interests = interests; ContactEmail = contactEmail; + Country = country; + City = city; + DateOfBirth = dateOfBirth; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserSettingsNotFoundException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserSettingsNotFoundException.cs new file mode 100644 index 000000000..73d773cc3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserSettingsNotFoundException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UserSettingsNotFoundException : AppException + { + public override string Code { get; } = "user_settings_not_found"; + public Guid Id { get; } + + public UserSettingsNotFoundException(Guid id) : base($"User settings for student with id: {id} were not found.") + { + Id = id; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithGalleryImages.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithGalleryImages.cs new file mode 100644 index 000000000..0b3a221e3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithGalleryImages.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetStudentWithGalleryImages : IQuery + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithVisibilitySettings.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithVisibilitySettings.cs new file mode 100644 index 000000000..4ce9c23ec --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetStudentWithVisibilitySettings.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetStudentWithVisibilitySettings : IQuery + { + public Guid StudentId { get; set; } + + public GetStudentWithVisibilitySettings(Guid studentId) + { + StudentId = studentId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserSettings.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserSettings.cs new file mode 100644 index 000000000..0edc10209 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserSettings.cs @@ -0,0 +1,13 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetUserSettings : IQuery + { + public Guid StudentId { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Education.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Education.cs new file mode 100644 index 000000000..80c6de7a9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Education.cs @@ -0,0 +1,20 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public class Education + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + + public Education(string institutionName, string degree, DateTime startDate, DateTime endDate, string description) + { + InstitutionName = institutionName; + Degree = degree; + StartDate = startDate; + EndDate = endDate; + Description = description; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/FrontendVersion.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/FrontendVersion.cs new file mode 100644 index 000000000..8ef5c0c23 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/FrontendVersion.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum FrontendVersion + { + Auto, + DarkMode, + LightMode, + SystemMode, + Default + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/GalleryImage.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/GalleryImage.cs new file mode 100644 index 000000000..8007f3133 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/GalleryImage.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class GalleryImage + { + public Guid ImageId { get; private set; } + public string ImageUrl { get; private set; } + public DateTime DateAdded { get; private set; } + + public GalleryImage(Guid imageId, string imageUrl, DateTime dateAdded) + { + ImageId = imageId; + ImageUrl = imageUrl ?? throw new ArgumentNullException(nameof(imageUrl)); + DateAdded = dateAdded; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Interest.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Interest.cs new file mode 100644 index 000000000..003e5f285 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Interest.cs @@ -0,0 +1,482 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum Interest + { + Acting, + Airsoft, + Archery, + Astronomy, + Automotive, + Backpacking, + Badminton, + Baking, + Ballet, + Barbecue, + Basketball, + BeachVolleyball, + BirdWatching, + Blogging, + BoardGames, + BookClubs, + Bowling, + Boxing, + BrewingBeer, + BungeeJumping, + Calligraphy, + Camping, + Canoeing, + CardGames, + Chess, + Coding, + Collecting, + Comedy, + ComicBooks, + Cooking, + Crafting, + Cricket, + Crossfit, + Cycling, + Dancing, + Debate, + Darts, + DiningOut, + DIYProjects, + Drawing, + Embroidery, + Esports, + FantasySports, + Fashion, + FilmMaking, + Fishing, + Fitness, + FloralArranging, + FlyingDrones, + Football, + Gardening, + Genealogy, + Geocaching, + GlassBlowing, + Golf, + GourmetFood, + Gymnastics, + Hiking, + History, + Hockey, + HomeBrewing, + HorseRiding, + Hunting, + IceSkating, + Improv, + Investing, + JewelryMaking, + Juggling, + Kayaking, + Kitesurfing, + Knitting, + Lacrosse, + Landscaping, + LaserTag, + LegoBuilding, + Magic, + MartialArts, + Meditation, + MetalDetecting, + ModelBuilding, + MountainBiking, + Movies, + Music, + Networking, + Origami, + Painting, + Parkour, + PetCare, + Philately, + Philosophy, + Photography, + Piano, + Pilates, + PingPong, + Podcasts, + Poetry, + Pottery, + Puzzles, + Quilting, + Racing, + Rafting, + Reading, + RealEstate, + Robotics, + RockClimbing, + Rowing, + Rugby, + Running, + Sailing, + ScubaDiving, + Sewing, + Singing, + Skateboarding, + Skiing, + SkyDiving, + Snowboarding, + SoapMaking, + Soccer, + Spirituality, + StandUpComedy, + Surfing, + Swimming, + TableTennis, + Taekwondo, + TaiChi, + Tennis, + Theater, + Travel, + Trekking, + Trivia, + VideoGames, + Volleyball, + Volunteering, + Walking, + WatchingMovies, + WatchingSports, + WaterPolo, + Weaving, + WeightLifting, + WineTasting, + WoodWorking, + Writing, + Yoga, + Zumba, + ArcheryTag, + Astrophotography, + AutoRacing, + BadmintonSingles, + Beachcombing, + BirdPhotography, + BoardgameDesign, + Bodybuilding, + BookWriting, + BowlingTeams, + CampingHiking, + CanoeCamping, + CarShows, + CheeseMaking, + ChessTournaments, + ClaySculpting, + Climbing, + ComputerProgramming, + Concerts, + CraftBeer, + CreativeWriting, + Crochet, + DanceTeams, + DiningExperiences, + DIYHomeImprovement, + DollCollecting, + DroneRacing, + EnduranceSports, + EventPlanning, + Exercise, + FantasyFootball, + FineDining, + FloralDesign, + FoodTasting, + GardeningVegetables, + Gemology, + GhostHunting, + GlassArt, + GolfCourses, + GourmetCooking, + GymWorkouts, + Handball, + HistoricReenactments, + HomeRenovation, + HotAirBallooning, + IceFishing, + IndoorClimbing, + JewelryDesign, + JigsawPuzzles, + Judo, + KayakFishing, + KiteFlying, + KnittingClubs, + LeatherWorking, + LegoCompetitions, + Lighthouses, + MagicShows, + MakeupArtistry, + MartialArtsTournaments, + MeditationRetreats, + MiniatureBuilding, + ModelAircraft, + ModelRailroading, + MountainClimbing, + MovieProduction, + MusicBands, + NatureHiking, + OrigamiArt, + PaintingClasses, + Paragliding, + ParkPhotography, + PetShows, + Philanthropy, + PhilatelyClubs, + PhilosophyGroups, + PianoConcerts, + PilatesClasses, + Poker, + PoliticalDebates, + PotteryClasses, + Powerlifting, + PuzzleChallenges, + QuiltingGroups, + RacketSports, + RaftingTrips, + ReadingCircles, + RealEstateInvesting, + Reenactments, + RoboticsClubs, + RockCollecting, + RunningMarathons, + SailboatRacing, + ScavengerHunts, + SculptureArt, + SkateParks, + SkiMountaineering, + SkyPhotography, + SoapBoxRacing, + Softball, + SpeedSkating, + SpiritualRetreats, + SportsAnalytics, + SportsPhotography, + Storytelling, + SurfPhotography, + SurfFishing, + SurvivalSkills, + TableFootball, + TaiChiClasses, + TalentShows, + Technology, + TennisMatches, + TheaterProduction, + TheatricalMakeup, + TravelPhotography, + TreeClimbing, + TriviaNights, + TropicalFish, + UrbanExploring, + VideoProduction, + VirtualReality, + VolunteerFirefighting, + Wakeboarding, + WaterAerobics, + WaterSkiing, + WeightTraining, + WineMaking, + WoodCarving, + WordGames, + Wrestling, + WritingPoetry, + YogaClasses, + ZipLining, + Zookeeping, + AirRifle, + AnimalRescue, + Aquascaping, + ArtGalleries, + BakingClasses, + BallroomDancing, + BaristaSkills, + Beekeeping, + BikeRacing, + Blacksmithing, + BoatBuilding, + BookRestoration, + BonsaiGrowing, + BrewingCider, + BullRiding, + ButterflyWatching, + CalligraphyArt, + CandleMaking, + CardTricks, + CaveExploring, + CelticMusic, + ChainsawCarving, + CircusArts, + ClassicCars, + ClayPigeonShooting, + Clowning, + ComicCollecting, + CompetitiveEating, + Cosplaying, + CountryDancing, + CraftFairs, + CricketUmpiring, + CrossCountrySkiing, + Cupping, + CurlingTeams, + DanceFitness, + DanceHall, + Design, + DogTraining, + Dominoes, + DragonBoatRacing, + ElectricVehicles, + Electronics, + FireDancing, + FirePoi, + FleaMarkets, + FolkDancing, + FoodStalls, + Forestry, + GameDesign, + GemCutting, + GeocachingHikes, + GiantPuppets, + GlassEtching, + Golfing, + GourmetCatering, + GreenEnergy, + Guitar, + Hackathons, + HandmadeCards, + HerbGardening, + HighlandGames, + HistoricalTours, + HomeAutomation, + Horticulture, + IceDiving, + InlineHockey, + InstrumentMaking, + JewelryMakingClasses, + KettlebellTraining, + KiteSurfing, + LandArt, + LaserShows, + Leathercraft, + LightArt, + LiveActionRolePlaying, + Macrame, + MarathonRunning, + MarineBiology, + MetalArt, + Microscopy, + MineralCollecting, + MixedMartialArts, + MockTrials, + ModelShips, + Monologues, + MosaicArt, + MountainRunning, + MuseumTours, + MushroomHunting, + NatureConservation, + NeedleFelting, + Needlepoint, + NightPhotography, + OrienteeringSports, + OutdoorCooking, + Paddleboarding, + PaperMaking, + ParkourClasses, + PerfumeMaking, + PetTraining, + PhilosophyLectures, + PhotographyClasses, + PianoLessons, + Pinball, + PinballMachines, + PlayingDrums, + PlayingGuitar, + PlayingSaxophone, + PlayingViolin, + PoleDancing, + PotluckDinners, + Powerboating, + PrecisionShooting, + PublicSpeaking, + Puppetry, + Quilling, + RallyDriving, + RCBoats, + RCPlanes, + RCTrucks, + RockMusic, + Rockhounding, + Rocketry, + RollerHockey, + RowingClubs, + RubiksCube, + RunningClubs, + Sailboard, + SalsaDancing, + SandSculpting, + ScienceFairs, + ScrapMetalArt, + Scrying, + SecretSanta, + SewingClasses, + Shogi, + ShrubSculpting, + SkyWatching, + SlotCars, + SlowFood, + SoapSculpting, + SoftballLeagues, + SolarEnergy, + Spelunking, + SpokenWord, + SportsScience, + SquashMatches, + StainedGlass, + StandUpPaddleboarding, + Stargazing, + Steampunk, + StoneSkipping, + StreetDancing, + StreetPerforming, + SunPhotography, + SurfKayaking, + SwingDancing, + SwordFighting, + TableHockey, + TableSoccer, + TangoDancing, + TattooArt, + TeamSports, + TechnicalWriting, + TextileArt, + TheaterActing, + TheatreHistory, + ThrowingDarts, + TimeCapsules, + ToyCollecting, + TraditionalGames, + TrampoliningTeams, + Trainspotting, + Triathlons, + TroutFishing, + TruckRacing, + UrbanGardening, + VideoBlogging, + VintageCars, + Volcanology, + WakeSurfing, + WaterBalloonFight, + Watercolors, + WheelchairBasketball, + Whittling, + WildflowerPhotography, + WildlifePhotography, + Windsurfing, + Woodburning, + WordSearch, + WorldBuilding, + WritingFiction, + WritingNonfiction, + WritingWorkshops, + Yachting, + YogaRetreats, + ZumbaFitness + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Language.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Language.cs new file mode 100644 index 000000000..16139aea5 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Language.cs @@ -0,0 +1,52 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum Language + { + English, + Spanish, + French, + German, + Chinese, + Japanese, + Korean, + Italian, + Russian, + Portuguese, + Arabic, + Hindi, + Bengali, + Punjabi, + Javanese, + Vietnamese, + Telugu, + Marathi, + Tamil, + Urdu, + Turkish, + Persian, + Gujarati, + Polish, + Ukrainian, + Dutch, + Greek, + Czech, + Swedish, + Hungarian, + Danish, + Finnish, + Norwegian, + Hebrew, + Malay, + Indonesian, + Thai, + Filipino, + Swahili, + Zulu, + Xhosa, + Yoruba, + Igbo, + Amharic, + Somali, + Hausa + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/PreferredLanguage.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/PreferredLanguage.cs new file mode 100644 index 000000000..bdcd3fd3a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/PreferredLanguage.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum PreferredLanguage + { + English, + Polish, + Ukrainian, + Spanish, + French, + German, + Chinese, + Japanese, + Russian, + Other + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs index 6488e43d0..0bbb02781 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -10,42 +10,46 @@ public class Student : AggregateRoot { private ISet _interestedInEvents = new HashSet(); private ISet _signedUpEvents = new HashSet(); - private ISet _galleryOfImages = new HashSet(); - private ISet _languages = new HashSet(); - private ISet _interests = new HashSet(); + private ISet _languages = new HashSet(); + private ISet _interests = new HashSet(); + private ISet _education = new HashSet(); + private ISet _work = new HashSet(); public string Email { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } public string FullName => $"{FirstName} {LastName}"; - public int NumberOfFriends { get; private set; } public string ProfileImageUrl { get; private set; } public string Description { get; private set; } public DateTime? DateOfBirth { get; private set; } public bool EmailNotifications { get; private set; } public bool IsBanned { get; private set; } - public bool IsOrganizer { get; private set; } public State State { get; private set; } public DateTime CreatedAt { get; private set; } public string ContactEmail { get; private set; } public string BannerUrl { get; private set; } - public IEnumerable GalleryOfImageUrls - { - get => _galleryOfImages; - set => _galleryOfImages = new HashSet(value ?? Enumerable.Empty()); - } - public string Education { get; private set; } - public string WorkPosition { get; private set; } - public string Company { get; private set; } - public IEnumerable Languages + public string PhoneNumber { get; private set; } + public string Country { get; private set; } + public string City { get; private set; } + public IEnumerable Languages { get => _languages; - set => _languages = new HashSet(value ?? Enumerable.Empty()); + set => _languages = new HashSet(value ?? Enumerable.Empty()); } - public IEnumerable Interests + public IEnumerable Interests { get => _interests; - set => _interests = new HashSet(value ?? Enumerable.Empty()); + set => _interests = new HashSet(value ?? Enumerable.Empty()); + } + public IEnumerable Education + { + get => _education; + set => _education = new HashSet(value ?? Enumerable.Empty()); + } + public IEnumerable Work + { + get => _work; + set => _work = new HashSet(value ?? Enumerable.Empty()); } public bool IsTwoFactorEnabled { get; private set; } public string TwoFactorSecret { get; private set; } @@ -60,48 +64,39 @@ public IEnumerable SignedUpEvents set => _signedUpEvents = new HashSet(value ?? Enumerable.Empty()); } - public Student(Guid id, string firstName, string lastName, string email, DateTime createdAt) - : this(id, email, createdAt, firstName, lastName, 0, string.Empty, string.Empty, null, - false, false, false, State.Unverified, Enumerable.Empty(), Enumerable.Empty(), null, - Enumerable.Empty(), null, null, null, Enumerable.Empty(), Enumerable.Empty(), - false, null, null) - { - CheckFullName(firstName, lastName); - } - public Student(Guid id, string email, DateTime createdAt, string firstName, string lastName, - int numberOfFriends, string profileImageUrl, string description, DateTime? dateOfBirth, - bool emailNotifications, bool isBanned, bool isOrganizer, State state, + string profileImageUrl, string description, DateTime? dateOfBirth, + bool emailNotifications, bool isBanned, State state, IEnumerable interestedInEvents, IEnumerable signedUpEvents, - string bannerUrl, IEnumerable galleryOfImageUrls, string education, - string workPosition, string company, IEnumerable languages, IEnumerable interests, - bool isTwoFactorEnabled, string twoFactorSecret, string contactEmail = null) + string bannerUrl, IEnumerable education, IEnumerable work, + IEnumerable languages, IEnumerable interests, + bool isTwoFactorEnabled, string twoFactorSecret, string contactEmail, + string phoneNumber, string country, string city) { Id = id; Email = email; CreatedAt = createdAt; FirstName = firstName; LastName = lastName; - NumberOfFriends = numberOfFriends; ProfileImageUrl = profileImageUrl; Description = description; DateOfBirth = dateOfBirth; EmailNotifications = emailNotifications; IsBanned = isBanned; - IsOrganizer = isOrganizer; State = state; InterestedInEvents = interestedInEvents ?? Enumerable.Empty(); SignedUpEvents = signedUpEvents ?? Enumerable.Empty(); BannerUrl = bannerUrl; - GalleryOfImageUrls = galleryOfImageUrls ?? Enumerable.Empty(); - Education = education; - WorkPosition = workPosition; - Company = company; - Languages = languages ?? Enumerable.Empty(); - Interests = interests ?? Enumerable.Empty(); + Education = education ?? Enumerable.Empty(); + Work = work ?? Enumerable.Empty(); + Languages = languages ?? Enumerable.Empty(); + Interests = interests ?? Enumerable.Empty(); IsTwoFactorEnabled = isTwoFactorEnabled; TwoFactorSecret = twoFactorSecret; ContactEmail = contactEmail; + PhoneNumber = phoneNumber; + Country = country; + City = city; } public void SetIncomplete() => SetState(State.Incomplete); @@ -136,7 +131,8 @@ public void CompleteRegistration(string profileImageUrl, string description, AddEvent(new StudentRegistrationCompleted(this)); } - public void Update(string firstName, string lastName, string profileImageUrl, string description, bool emailNotifications, string contactEmail) + public void Update(string firstName, string lastName, string description, + bool emailNotifications, string contactEmail, string phoneNumber, string country, string city, DateTime? dateOfBirth) { CheckFullName(firstName, lastName); CheckDescription(description); @@ -148,10 +144,13 @@ public void Update(string firstName, string lastName, string profileImageUrl, st FirstName = firstName; LastName = lastName; - ProfileImageUrl = profileImageUrl; Description = description; EmailNotifications = emailNotifications; ContactEmail = contactEmail; + PhoneNumber = phoneNumber; + Country = country; + City = city; + DateOfBirth = dateOfBirth; AddEvent(new StudentUpdated(this)); } @@ -168,63 +167,27 @@ public void UpdateBannerUrl(string bannerUrl) AddEvent(new StudentBannerUpdated(this)); } - public void AddGalleryImageUrl(string imageUrl) - { - _galleryOfImages.Add(imageUrl); - AddEvent(new StudentGalleryOfImagesUpdated(this)); - } - - public void UpdateGalleryOfImageUrls(IEnumerable galleryOfImageUrls) - { - GalleryOfImageUrls = new HashSet(galleryOfImageUrls ?? Enumerable.Empty()); - AddEvent(new StudentGalleryOfImagesUpdated(this)); - } - - public void RemoveGalleryImage(string imageUrl) - { - if (!_galleryOfImages.Contains(imageUrl)) - { - throw new StudentGalleryImageNotFoundException(Id, imageUrl); - } - - _galleryOfImages = new HashSet(_galleryOfImages.Select(url => url == imageUrl ? string.Empty : url)); - AddEvent(new StudentGalleryOfImagesUpdated(this)); - } - - public void RemoveBannerImage() - { - - BannerUrl = string.Empty; - AddEvent(new StudentBannerUpdated(this)); - } - - public void UpdateEducation(string education) + public void UpdateEducation(IEnumerable education) { - Education = education; + Education = new HashSet(education ?? Enumerable.Empty()); AddEvent(new StudentEducationUpdated(this)); } - public void UpdateWorkPosition(string workPosition) - { - WorkPosition = workPosition; - AddEvent(new StudentWorkPositionUpdated(this)); - } - - public void UpdateCompany(string company) + public void UpdateWork(IEnumerable work) { - Company = company; - AddEvent(new StudentCompanyUpdated(this)); + Work = new HashSet(work ?? Enumerable.Empty()); + AddEvent(new StudentWorkUpdated(this)); } - public void UpdateLanguages(IEnumerable languages) + public void UpdateLanguages(IEnumerable languages) { - Languages = new HashSet(languages ?? Enumerable.Empty()); + Languages = new HashSet(languages ?? Enumerable.Empty()); AddEvent(new StudentLanguagesUpdated(this)); } - public void UpdateInterests(IEnumerable interests) + public void UpdateInterests(IEnumerable interests) { - Interests = new HashSet(interests ?? Enumerable.Empty()); + Interests = new HashSet(interests ?? Enumerable.Empty()); AddEvent(new StudentInterestsUpdated(this)); } @@ -234,6 +197,18 @@ public void UpdateContactEmail(string contactEmail) AddEvent(new StudentUpdated(this)); } + public void RemoveProfileImage() + { + ProfileImageUrl = string.Empty; + AddEvent(new StudentProfileImageRemoved(this)); + } + + public void RemoveBannerImage() + { + BannerUrl = string.Empty; + AddEvent(new StudentBannerImageRemoved(this)); + } + public void EnableTwoFactorAuthentication(string twoFactorSecret) { if (string.IsNullOrWhiteSpace(twoFactorSecret)) @@ -269,6 +244,14 @@ private void CheckDescription(string description) // } } + public void VerifyEmail(string email, DateTime verifiedAt) + { + if (Email == email) + { + State = State.Valid; + } + } + private void CheckDateOfBirth(DateTime dateOfBirth, DateTime now) { if (dateOfBirth >= now) @@ -319,15 +302,13 @@ public void RemoveSignedUpEvent(Guid eventId) } } - public void RemoveProfileImage() - { - ProfileImageUrl = string.Empty; - } - public void Ban() => IsBanned = true; public void Unban() => IsBanned = false; - public void GrantOrganizerRights() => IsOrganizer = true; - public void RevokeOrganizerRights() => IsOrganizer = false; + public void SetEmailNotifications(bool emailNotifications) + { + EmailNotifications = emailNotifications; + AddEvent(new StudentUpdated(this)); + } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserAvailableSettings.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserAvailableSettings.cs new file mode 100644 index 000000000..440200714 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserAvailableSettings.cs @@ -0,0 +1,74 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserAvailableSettings + { + public Visibility CreatedAtVisibility { get; private set; } + public Visibility DateOfBirthVisibility { get; private set; } + public Visibility InterestedInEventsVisibility { get; private set; } + public Visibility SignedUpEventsVisibility { get; private set; } + public Visibility EducationVisibility { get; private set; } + public Visibility WorkPositionVisibility { get; private set; } + public Visibility LanguagesVisibility { get; private set; } + public Visibility InterestsVisibility { get; private set; } + public Visibility ContactEmailVisibility { get; private set; } + public Visibility PhoneNumberVisibility { get; private set; } + public FrontendVersion FrontendVersion { get; private set; } + public PreferredLanguage PreferredLanguage { get; private set; } + + public UserAvailableSettings() + { + CreatedAtVisibility = Visibility.Everyone; + DateOfBirthVisibility = Visibility.Everyone; + InterestedInEventsVisibility = Visibility.Everyone; + SignedUpEventsVisibility = Visibility.Everyone; + EducationVisibility = Visibility.Everyone; + WorkPositionVisibility = Visibility.Everyone; + LanguagesVisibility = Visibility.Everyone; + InterestsVisibility = Visibility.Everyone; + ContactEmailVisibility = Visibility.Everyone; + PhoneNumberVisibility = Visibility.Everyone; + FrontendVersion = FrontendVersion.Default; + PreferredLanguage = PreferredLanguage.English; + } + + public UserAvailableSettings(Visibility createdAtVisibility, Visibility dateOfBirthVisibility, Visibility interestedInEventsVisibility, + Visibility signedUpEventsVisibility, Visibility educationVisibility, Visibility workPositionVisibility, + Visibility languagesVisibility, Visibility interestsVisibility, Visibility contactEmailVisibility, + Visibility phoneNumberVisibility, FrontendVersion frontendVersion, PreferredLanguage preferredLanguage) + { + CreatedAtVisibility = createdAtVisibility; + DateOfBirthVisibility = dateOfBirthVisibility; + InterestedInEventsVisibility = interestedInEventsVisibility; + SignedUpEventsVisibility = signedUpEventsVisibility; + EducationVisibility = educationVisibility; + WorkPositionVisibility = workPositionVisibility; + LanguagesVisibility = languagesVisibility; + InterestsVisibility = interestsVisibility; + ContactEmailVisibility = contactEmailVisibility; + PhoneNumberVisibility = phoneNumberVisibility; + FrontendVersion = frontendVersion; + PreferredLanguage = preferredLanguage; + } + + public void UpdateSettings(Visibility createdAtVisibility, Visibility dateOfBirthVisibility, Visibility interestedInEventsVisibility, + Visibility signedUpEventsVisibility, Visibility educationVisibility, Visibility workPositionVisibility, + Visibility languagesVisibility, Visibility interestsVisibility, Visibility contactEmailVisibility, + Visibility phoneNumberVisibility, FrontendVersion frontendVersion, PreferredLanguage preferredLanguage) + { + CreatedAtVisibility = createdAtVisibility; + DateOfBirthVisibility = dateOfBirthVisibility; + InterestedInEventsVisibility = interestedInEventsVisibility; + SignedUpEventsVisibility = signedUpEventsVisibility; + EducationVisibility = educationVisibility; + WorkPositionVisibility = workPositionVisibility; + LanguagesVisibility = languagesVisibility; + InterestsVisibility = interestsVisibility; + ContactEmailVisibility = contactEmailVisibility; + PhoneNumberVisibility = phoneNumberVisibility; + FrontendVersion = frontendVersion; + PreferredLanguage = preferredLanguage; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserGallery.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserGallery.cs new file mode 100644 index 000000000..894b368d5 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserGallery.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Students.Core.Exceptions; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserGallery + { + public Guid UserId { get; private set; } + private ISet _galleryOfImages = new HashSet(); + + public IEnumerable GalleryOfImages + { + get => _galleryOfImages; + private set => _galleryOfImages = new HashSet(value ?? Enumerable.Empty()); + } + + public UserGallery(Guid userId) + { + UserId = userId; + } + + public void AddGalleryImage(Guid imageId, string imageUrl) + { + _galleryOfImages.Add(new GalleryImage(imageId, imageUrl, DateTime.UtcNow)); + } + + public void UpdateGalleryOfImages(IEnumerable galleryOfImages) + { + GalleryOfImages = new HashSet(galleryOfImages ?? Enumerable.Empty()); + } + + public void RemoveGalleryImage(Guid imageId) + { + var image = _galleryOfImages.FirstOrDefault(img => img.ImageId == imageId); + if (image == null) + { + throw new StudentGalleryImageNotFoundException(UserId, imageId.ToString()); + } + + _galleryOfImages.Remove(image); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserSettings.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserSettings.cs new file mode 100644 index 000000000..d5687725e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserSettings.cs @@ -0,0 +1,23 @@ +using System; +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserSettings : AggregateRoot + { + public Guid StudentId { get; private set; } + public UserAvailableSettings AvailableSettings { get; private set; } + + public UserSettings(Guid studentId, UserAvailableSettings availableSettings) + { + StudentId = studentId; + AvailableSettings = availableSettings ?? new UserAvailableSettings(); + } + + public void UpdateSettings(UserAvailableSettings availableSettings) + { + AvailableSettings = availableSettings ?? new UserAvailableSettings(); + AddEvent(new UserSettingsUpdated(this)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Visibility.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Visibility.cs new file mode 100644 index 000000000..87640d4de --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Visibility.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public enum Visibility + { + Everyone, + Connections, + NoOne + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Work.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Work.cs new file mode 100644 index 000000000..ac494d301 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Work.cs @@ -0,0 +1,20 @@ +namespace MiniSpace.Services.Students.Core.Entities +{ + public class Work + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + + public Work(string company, string position, DateTime startDate, DateTime endDate, string description) + { + Company = company; + Position = position; + StartDate = startDate; + EndDate = endDate; + Description = description; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentBannerImageRemoved.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentBannerImageRemoved.cs new file mode 100644 index 000000000..4177f5038 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentBannerImageRemoved.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Core.Events +{ + [ExcludeFromCodeCoverage] + public class StudentBannerImageRemoved : IDomainEvent + { + public Student Student { get; } + + public StudentBannerImageRemoved(Student student) + { + Student = student; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentProfileImageRemoved.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentProfileImageRemoved.cs new file mode 100644 index 000000000..aa6bc7b9a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentProfileImageRemoved.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Core.Events +{ + [ExcludeFromCodeCoverage] + public class StudentProfileImageRemoved : IDomainEvent + { + public Student Student { get; } + + public StudentProfileImageRemoved(Student student) + { + Student = student; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentWorkUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentWorkUpdated.cs new file mode 100644 index 000000000..e474410d9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/StudentWorkUpdated.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Core.Events +{ + [ExcludeFromCodeCoverage] + public class StudentWorkUpdated : IDomainEvent + { + public Student Student { get; } + + public StudentWorkUpdated(Student student) + { + Student = student; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserSettingsUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserSettingsUpdated.cs new file mode 100644 index 000000000..ba31bfe10 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserSettingsUpdated.cs @@ -0,0 +1,15 @@ +using System; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class UserSettingsUpdated : IDomainEvent + { + public UserSettings UserSettings { get; } + + public UserSettingsUpdated(UserSettings userSettings) + { + UserSettings = userSettings; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IReadUserRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IReadUserRepository.cs new file mode 100644 index 000000000..7afa6b97f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IReadUserRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IReadUserRepository + { + Task GetAsync(Guid id); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserGalleryRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserGalleryRepository.cs new file mode 100644 index 000000000..9ef43ab9c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserGalleryRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserGalleryRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserGallery userGallery); + Task UpdateAsync(UserGallery userGallery); + Task DeleteAsync(Guid userId); + Task> GetGalleryImagesAsync(Guid userId); + Task AddGalleryImageAsync(Guid userId, GalleryImage galleryImage); + Task RemoveGalleryImageAsync(Guid userId, Guid imageId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserSettingsRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserSettingsRepository.cs new file mode 100644 index 000000000..aaa7b1498 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserSettingsRepository.cs @@ -0,0 +1,13 @@ +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserSettingsRepository + { + Task GetUserSettingsAsync(Guid studentId); + Task AddUserSettingsAsync(UserSettings userSettings); + Task UpdateUserSettingsAsync(UserSettings userSettings); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs index 3388aedb0..d0e50d304 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs @@ -39,7 +39,9 @@ using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; using MiniSpace.Services.Students.Infrastructure.Mongo.Repositories; using MiniSpace.Services.Students.Infrastructure.Services; +using MongoDB.Driver; using System.Diagnostics.CodeAnalysis; +using Convey.Types; namespace MiniSpace.Services.Students.Infrastructure { @@ -50,6 +52,8 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); @@ -75,6 +79,8 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddHandlersLogging() .AddMongoRepository("students") .AddMongoRepository("user-notifications") + .AddMongoRepository("user-settings") + .AddMongoRepository("user-gellery") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); @@ -94,6 +100,8 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() .SubscribeEvent() .SubscribeEvent() .SubscribeEvent() @@ -102,10 +110,10 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeEvent() .SubscribeEvent() .SubscribeEvent() - .SubscribeEvent() - .SubscribeEvent() .SubscribeEvent() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); return app; } @@ -145,5 +153,6 @@ internal static string GetSpanContext(this IMessageProperties messageProperties, return string.Empty; } + } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs index 06c4680a9..66b3e1374 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -76,18 +76,6 @@ private static IReadOnlyDictionary MessageTemplates { After = "A student with id: {UserId} has been unbanned." } - }, - { - typeof(OrganizerRightsGranted), new HandlerLogTemplate - { - After = "Organizer rights has been granted for student with id: {UserId}." - } - }, - { - typeof(OrganizerRightsRevoked), new HandlerLogTemplate - { - After = "Organizer rights has been revoked for student with id: {UserId}." - } } }; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/EducationDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/EducationDocument.cs new file mode 100644 index 000000000..a30fc4587 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/EducationDocument.cs @@ -0,0 +1,18 @@ +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class EducationDocument + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs index 743de41c4..233ab83e1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/Extensions.cs @@ -1,11 +1,12 @@ using MiniSpace.Services.Students.Application.Dto; using MiniSpace.Services.Students.Core.Entities; -using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Linq; namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents { - [ExcludeFromCodeCoverage] - public static class Extensions + public static class Extensions { public static Student AsEntity(this StudentDocument document) => new Student( @@ -14,26 +15,25 @@ public static Student AsEntity(this StudentDocument document) document.CreatedAt, document.FirstName, document.LastName, - document.NumberOfFriends, document.ProfileImageUrl, document.Description, document.DateOfBirth, document.EmailNotifications, document.IsBanned, - document.IsOrganizer, document.State, document.InterestedInEvents, document.SignedUpEvents, document.BannerUrl, - document.GalleryOfImageUrls, - document.Education, - document.WorkPosition, - document.Company, - document.Languages, - document.Interests, + document.Education.Select(e => new Education(e.InstitutionName, e.Degree, e.StartDate, e.EndDate, e.Description)), + document.Work.Select(w => new Work(w.Company, w.Position, w.StartDate, w.EndDate, w.Description)), + document.Languages.Select(l => Enum.Parse(l.ToString())).ToList(), // Convert string to enum + document.Interests.Select(i => Enum.Parse(i.ToString())).ToList(), document.IsTwoFactorEnabled, document.TwoFactorSecret, - document.ContactEmail + document.ContactEmail, + document.PhoneNumber, + document.Country, + document.City ); public static StudentDocument AsDocument(this Student entity) @@ -43,27 +43,40 @@ public static StudentDocument AsDocument(this Student entity) Email = entity.Email, FirstName = entity.FirstName, LastName = entity.LastName, - NumberOfFriends = entity.NumberOfFriends, ProfileImageUrl = entity.ProfileImageUrl, Description = entity.Description, DateOfBirth = entity.DateOfBirth, EmailNotifications = entity.EmailNotifications, IsBanned = entity.IsBanned, - IsOrganizer = entity.IsOrganizer, State = entity.State, CreatedAt = entity.CreatedAt, InterestedInEvents = entity.InterestedInEvents, SignedUpEvents = entity.SignedUpEvents, BannerUrl = entity.BannerUrl, - GalleryOfImageUrls = entity.GalleryOfImageUrls, - Education = entity.Education, - WorkPosition = entity.WorkPosition, - Company = entity.Company, - Languages = entity.Languages, - Interests = entity.Interests, + Education = entity.Education.Select(e => new EducationDocument + { + InstitutionName = e.InstitutionName, + Degree = e.Degree, + StartDate = e.StartDate, + EndDate = e.EndDate, + Description = e.Description + }), + Work = entity.Work.Select(w => new WorkDocument + { + Company = w.Company, + Position = w.Position, + StartDate = w.StartDate, + EndDate = w.EndDate, + Description = w.Description + }), + Languages = entity.Languages.Select(l => l.ToString()), + Interests = entity.Interests.Select(i => i.ToString()), IsTwoFactorEnabled = entity.IsTwoFactorEnabled, TwoFactorSecret = entity.TwoFactorSecret, - ContactEmail = entity.ContactEmail + ContactEmail = entity.ContactEmail, + PhoneNumber = entity.PhoneNumber, + Country = entity.Country, + City = entity.City, }; public static StudentDto AsDto(this StudentDocument document) @@ -73,31 +86,43 @@ public static StudentDto AsDto(this StudentDocument document) Email = document.Email, FirstName = document.FirstName, LastName = document.LastName, - NumberOfFriends = document.NumberOfFriends, ProfileImageUrl = document.ProfileImageUrl, Description = document.Description, DateOfBirth = document.DateOfBirth, EmailNotifications = document.EmailNotifications, IsBanned = document.IsBanned, - IsOrganizer = document.IsOrganizer, State = document.State.ToString().ToLowerInvariant(), CreatedAt = document.CreatedAt, InterestedInEvents = document.InterestedInEvents, SignedUpEvents = document.SignedUpEvents, BannerUrl = document.BannerUrl, - GalleryOfImageUrls = document.GalleryOfImageUrls, - Education = document.Education, - WorkPosition = document.WorkPosition, - Company = document.Company, + Education = document.Education.Select(e => new EducationDto + { + InstitutionName = e.InstitutionName, + Degree = e.Degree, + StartDate = e.StartDate, + EndDate = e.EndDate, + Description = e.Description + }), + Work = document.Work.Select(w => new WorkDto + { + Company = w.Company, + Position = w.Position, + StartDate = w.StartDate, + EndDate = w.EndDate, + Description = w.Description + }), Languages = document.Languages, Interests = document.Interests, IsTwoFactorEnabled = document.IsTwoFactorEnabled, TwoFactorSecret = document.TwoFactorSecret, - ContactEmail = document.ContactEmail + ContactEmail = document.ContactEmail, + PhoneNumber = document.PhoneNumber, + Country = document.Country, + City = document.City, }; - - public static UserNotifications AsEntity(this UserNotificationsDocument document) + public static UserNotifications AsEntity(this UserNotificationsDocument document) => new UserNotifications( document.StudentId, document.NotificationPreferences @@ -127,7 +152,7 @@ public static NotificationPreferencesDto AsDto(this NotificationPreferences noti public static UserNotificationsDocument AsDocument(this NotificationPreferencesDto dto) => new UserNotificationsDocument { - Id = Guid.NewGuid(), + Id = Guid.NewGuid(), StudentId = dto.StudentId, NotificationPreferences = new NotificationPreferences( dto.AccountChanges, @@ -140,5 +165,67 @@ public static UserNotificationsDocument AsDocument(this NotificationPreferencesD dto.FriendsNotifications ) }; + + public static UserGallery AsEntity(this UserGalleryDocument document) + { + var gallery = new UserGallery(document.UserId); + foreach (var image in document.GalleryOfImages) + { + gallery.AddGalleryImage(image.ImageId, image.ImageUrl); + } + return gallery; + } + + public static UserGalleryDocument AsDocument(this UserGallery entity) + => new UserGalleryDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + GalleryOfImages = entity.GalleryOfImages.Select(gi => new GalleryImageDocument(gi.ImageId, gi.ImageUrl) { DateAdded = gi.DateAdded }) + }; + + public static UserGalleryDto AsDto(this UserGalleryDocument document) + => new UserGalleryDto(document.UserId, document.GalleryOfImages.Select(gi => new GalleryImageDto(gi.ImageId, gi.ImageUrl, gi.DateAdded))); + + public static UserSettings AsEntity(this UserSettingsDocument document) + => new UserSettings( + document.StudentId, + new UserAvailableSettings( + document.AvailableSettings.CreatedAtVisibility, + document.AvailableSettings.DateOfBirthVisibility, + document.AvailableSettings.InterestedInEventsVisibility, + document.AvailableSettings.SignedUpEventsVisibility, + document.AvailableSettings.EducationVisibility, + document.AvailableSettings.WorkPositionVisibility, + document.AvailableSettings.LanguagesVisibility, + document.AvailableSettings.InterestsVisibility, + document.AvailableSettings.ContactEmailVisibility, + document.AvailableSettings.PhoneNumberVisibility, + document.AvailableSettings.FrontendVersion, + document.AvailableSettings.PreferredLanguage + ) + ); + + public static UserSettingsDocument AsDocument(this UserSettings entity) + => new UserSettingsDocument + { + Id = Guid.NewGuid(), // Ensure a unique identifier is set + StudentId = entity.StudentId, + AvailableSettings = new UserAvailableSettingsDocument + { + CreatedAtVisibility = entity.AvailableSettings.CreatedAtVisibility, + DateOfBirthVisibility = entity.AvailableSettings.DateOfBirthVisibility, + InterestedInEventsVisibility = entity.AvailableSettings.InterestedInEventsVisibility, + SignedUpEventsVisibility = entity.AvailableSettings.SignedUpEventsVisibility, + EducationVisibility = entity.AvailableSettings.EducationVisibility, + WorkPositionVisibility = entity.AvailableSettings.WorkPositionVisibility, + LanguagesVisibility = entity.AvailableSettings.LanguagesVisibility, + InterestsVisibility = entity.AvailableSettings.InterestsVisibility, + ContactEmailVisibility = entity.AvailableSettings.ContactEmailVisibility, + PhoneNumberVisibility = entity.AvailableSettings.PhoneNumberVisibility, + FrontendVersion = entity.AvailableSettings.FrontendVersion, + PreferredLanguage = entity.AvailableSettings.PreferredLanguage + } + }; } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/GalleryImageDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/GalleryImageDocument.cs new file mode 100644 index 000000000..c9d8cef73 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/GalleryImageDocument.cs @@ -0,0 +1,20 @@ +using Convey.Types; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class GalleryImageDocument + { + public Guid ImageId { get; set; } + public string ImageUrl { get; set; } + public DateTime DateAdded { get; set; } + + public GalleryImageDocument(Guid imageId, string imageUrl) + { + ImageId = imageId; + ImageUrl = imageUrl ?? throw new ArgumentNullException(nameof(imageUrl)); + DateAdded = DateTime.UtcNow; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs index 9c19ae2e0..2c0b5e6c7 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -13,26 +13,25 @@ public class StudentDocument : IIdentifiable public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public int NumberOfFriends { get; set; } public string ProfileImageUrl { get; set; } 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 State State { get; set; } public DateTime CreatedAt { get; set; } public IEnumerable InterestedInEvents { get; set; } public IEnumerable SignedUpEvents { get; set; } public string BannerUrl { get; set; } - public IEnumerable GalleryOfImageUrls { get; set; } - public string Education { get; set; } - public string WorkPosition { get; set; } - public string Company { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } public IEnumerable Languages { get; set; } public IEnumerable Interests { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } - public string ContactEmail { get; set; } + public string ContactEmail { get; set; } + public string PhoneNumber { get; set; } + public string Country { get; set; } + public string City { get; set; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserAvailableSettingsDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserAvailableSettingsDocument.cs new file mode 100644 index 000000000..c7afa49c9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserAvailableSettingsDocument.cs @@ -0,0 +1,22 @@ +using MiniSpace.Services.Students.Core.Entities; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class UserAvailableSettingsDocument + { + public Visibility CreatedAtVisibility { get; set; } + public Visibility DateOfBirthVisibility { get; set; } + public Visibility InterestedInEventsVisibility { get; set; } + public Visibility SignedUpEventsVisibility { get; set; } + public Visibility EducationVisibility { get; set; } + public Visibility WorkPositionVisibility { get; set; } + public Visibility LanguagesVisibility { get; set; } + public Visibility InterestsVisibility { get; set; } + public Visibility ContactEmailVisibility { get; set; } + public Visibility PhoneNumberVisibility { get; set; } + public FrontendVersion FrontendVersion { get; set; } + public PreferredLanguage PreferredLanguage { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserGalleryDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserGalleryDocument.cs new file mode 100644 index 000000000..729d776a4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserGalleryDocument.cs @@ -0,0 +1,18 @@ +using Convey.Types; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserGalleryDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable GalleryOfImages { get; set; } + + public UserGalleryDocument() + { + GalleryOfImages = new List(); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserNotificationsDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserNotificationsDocument.cs index 9b7a32080..30a08c849 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserNotificationsDocument.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserNotificationsDocument.cs @@ -1,9 +1,11 @@ using Convey.Types; using MiniSpace.Services.Students.Core.Entities; using System; +using System.Diagnostics.CodeAnalysis; namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents { + [ExcludeFromCodeCoverage] public class UserNotificationsDocument : IIdentifiable { public Guid Id { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserSettingsDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserSettingsDocument.cs new file mode 100644 index 000000000..9f0b941cf --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserSettingsDocument.cs @@ -0,0 +1,15 @@ +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class UserSettingsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public UserAvailableSettingsDocument AvailableSettings { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/WorkDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/WorkDocument.cs new file mode 100644 index 000000000..488707092 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/WorkDocument.cs @@ -0,0 +1,18 @@ +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class WorkDocument + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentImagesHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentImagesHandler.cs deleted file mode 100644 index 3b6a563f5..000000000 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentImagesHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Convey.CQRS.Queries; -using Convey.Persistence.MongoDB; -using MiniSpace.Services.Students.Application.Dto; -using MiniSpace.Services.Students.Application.Exceptions; -using MiniSpace.Services.Students.Application.Queries; -using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Diagnostics.CodeAnalysis; - -namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers -{ - [ExcludeFromCodeCoverage] - public class GetStudentImagesHandler : IQueryHandler - { - private readonly IMongoRepository _studentRepository; - - public GetStudentImagesHandler(IMongoRepository studentRepository) - { - _studentRepository = studentRepository; - } - - public async Task HandleAsync(GetStudentImages query, CancellationToken cancellationToken) - { - var document = await _studentRepository.GetAsync(p => p.Id == query.StudentId); - if (document is null) - { - throw new StudentNotFoundException(query.StudentId); - } - - var studentImages = new StudentImagesDto( - document.Id, - document.BannerUrl, - document.GalleryOfImageUrls - ); - - return studentImages; - } - } -} - diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithGalleryImagesHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithGalleryImagesHandler.cs new file mode 100644 index 000000000..84beb0d0f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithGalleryImagesHandler.cs @@ -0,0 +1,43 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentWithGalleryImagesHandler : IQueryHandler + { + private readonly IMongoRepository _studentRepository; + private readonly IMongoRepository _galleryRepository; + + public GetStudentWithGalleryImagesHandler(IMongoRepository studentRepository, IMongoRepository galleryRepository) + { + _studentRepository = studentRepository; + _galleryRepository = galleryRepository; + } + + public async Task HandleAsync(GetStudentWithGalleryImages query, CancellationToken cancellationToken) + { + var studentDocument = await _studentRepository.GetAsync(p => p.Id == query.StudentId); + if (studentDocument == null) + { + return null; + } + + var galleryDocument = await _galleryRepository.GetAsync(g => g.UserId == query.StudentId); + var galleryImages = galleryDocument?.GalleryOfImages.Select(i => new GalleryImageDto(i.ImageId, i.ImageUrl, i.DateAdded)).ToList() ?? new List(); + + return new StudentWithGalleryImagesDto + { + Student = studentDocument.AsDto(), + GalleryImages = galleryImages + }; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithVisibilitySettingsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithVisibilitySettingsHandler.cs new file mode 100644 index 000000000..d4fdb3fc4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetStudentWithVisibilitySettingsHandler.cs @@ -0,0 +1,60 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetStudentWithVisibilitySettingsHandler : IQueryHandler + { + private readonly IMongoRepository _studentRepository; + private readonly IMongoRepository _settingsRepository; + + public GetStudentWithVisibilitySettingsHandler(IMongoRepository studentRepository, IMongoRepository settingsRepository) + { + _studentRepository = studentRepository; + _settingsRepository = settingsRepository; + } + + public async Task HandleAsync(GetStudentWithVisibilitySettings query, CancellationToken cancellationToken) + { + var studentDocument = await _studentRepository.GetAsync(p => p.Id == query.StudentId); + if (studentDocument == null) + { + return null; + } + + var settingsDocument = await _settingsRepository.GetAsync(s => s.StudentId == query.StudentId); + if (settingsDocument == null) + { + return null; + } + + var visibilitySettings = new AvailableSettingsDto + { + CreatedAtVisibility = settingsDocument.AvailableSettings.CreatedAtVisibility.ToString(), + DateOfBirthVisibility = settingsDocument.AvailableSettings.DateOfBirthVisibility.ToString(), + InterestedInEventsVisibility = settingsDocument.AvailableSettings.InterestedInEventsVisibility.ToString(), + SignedUpEventsVisibility = settingsDocument.AvailableSettings.SignedUpEventsVisibility.ToString(), + EducationVisibility = settingsDocument.AvailableSettings.EducationVisibility.ToString(), + WorkPositionVisibility = settingsDocument.AvailableSettings.WorkPositionVisibility.ToString(), + LanguagesVisibility = settingsDocument.AvailableSettings.LanguagesVisibility.ToString(), + InterestsVisibility = settingsDocument.AvailableSettings.InterestsVisibility.ToString(), + ContactEmailVisibility = settingsDocument.AvailableSettings.ContactEmailVisibility.ToString(), + PhoneNumberVisibility = settingsDocument.AvailableSettings.PhoneNumberVisibility.ToString(), + PreferredLanguage = settingsDocument.AvailableSettings.PreferredLanguage.ToString(), + FrontendVersion = settingsDocument.AvailableSettings.FrontendVersion.ToString() + }; + + return new StudentWithVisibilitySettingsDto + { + Student = studentDocument.AsDto(), + VisibilitySettings = visibilitySettings + }; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserSettingsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserSettingsHandler.cs new file mode 100644 index 000000000..a7359458e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserSettingsHandler.cs @@ -0,0 +1,47 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserSettingsHandler : IQueryHandler + { + private readonly IUserSettingsRepository _userSettingsRepository; + + public GetUserSettingsHandler(IUserSettingsRepository userSettingsRepository) + { + _userSettingsRepository = userSettingsRepository; + } + + public async Task HandleAsync(GetUserSettings query, CancellationToken cancellationToken) + { + var userSettings = await _userSettingsRepository.GetUserSettingsAsync(query.StudentId); + if (userSettings == null) + { + return null; + } + + return new UserSettingsDto + { + StudentId = userSettings.StudentId, + CreatedAtVisibility = userSettings.AvailableSettings.CreatedAtVisibility.ToString(), + DateOfBirthVisibility = userSettings.AvailableSettings.DateOfBirthVisibility.ToString(), + InterestedInEventsVisibility = userSettings.AvailableSettings.InterestedInEventsVisibility.ToString(), + SignedUpEventsVisibility = userSettings.AvailableSettings.SignedUpEventsVisibility.ToString(), + EducationVisibility = userSettings.AvailableSettings.EducationVisibility.ToString(), + WorkPositionVisibility = userSettings.AvailableSettings.WorkPositionVisibility.ToString(), + LanguagesVisibility = userSettings.AvailableSettings.LanguagesVisibility.ToString(), + InterestsVisibility = userSettings.AvailableSettings.InterestsVisibility.ToString(), + ContactEmailVisibility = userSettings.AvailableSettings.ContactEmailVisibility.ToString(), + PhoneNumberVisibility = userSettings.AvailableSettings.PhoneNumberVisibility.ToString(), + PreferredLanguage = userSettings.AvailableSettings.PreferredLanguage.ToString(), + FrontendVersion = userSettings.AvailableSettings.FrontendVersion.ToString() + }; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserGalleryRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserGalleryRepository.cs new file mode 100644 index 000000000..b6cec314d --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserGalleryRepository.cs @@ -0,0 +1,112 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class UserGalleryRepository : IUserGalleryRepository + { + private readonly IMongoRepository _repository; + + public UserGalleryRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userId); + return userGalleryDocument?.AsEntity(); + } + + public async Task AddAsync(UserGallery userGallery) + { + var userGalleryDocument = userGallery.AsDocument(); + await _repository.AddAsync(userGalleryDocument); + } + + public async Task UpdateAsync(UserGallery userGallery) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userGallery.UserId); + + if (userGalleryDocument == null) + { + userGalleryDocument = userGallery.AsDocument(); + await _repository.AddAsync(userGalleryDocument); + } + else + { + userGalleryDocument.GalleryOfImages = userGallery.GalleryOfImages.Select(g => new GalleryImageDocument(g.ImageId, g.ImageUrl)).ToList(); + await _repository.UpdateAsync(userGalleryDocument); + } + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + + public async Task> GetGalleryImagesAsync(Guid userId) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userId); + return userGalleryDocument?.GalleryOfImages.Select(g => new GalleryImage(g.ImageId, g.ImageUrl, g.DateAdded)) ?? Enumerable.Empty(); + } + + public async Task AddGalleryImageAsync(Guid userId, GalleryImage galleryImage) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userId); + + if (userGalleryDocument == null) + { + userGalleryDocument = new UserGalleryDocument + { + Id = Guid.NewGuid(), + UserId = userId, + GalleryOfImages = new List + { + new GalleryImageDocument(galleryImage.ImageId, galleryImage.ImageUrl) + } + }; + await _repository.AddAsync(userGalleryDocument); + } + else + { + var galleryImages = userGalleryDocument.GalleryOfImages.ToList(); + galleryImages.Add(new GalleryImageDocument(galleryImage.ImageId, galleryImage.ImageUrl)); + userGalleryDocument.GalleryOfImages = galleryImages; + await _repository.UpdateAsync(userGalleryDocument); + } + } + + public async Task RemoveGalleryImageAsync(Guid userId, Guid imageId) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userId); + + if (userGalleryDocument != null) + { + var galleryImages = userGalleryDocument.GalleryOfImages.Where(g => g.ImageId != imageId).ToList(); + userGalleryDocument.GalleryOfImages = galleryImages; + await _repository.UpdateAsync(userGalleryDocument); + } + } + + public async Task RemoveGalleryImageByUrlAsync(Guid userId, string imageUrl) + { + var userGalleryDocument = await _repository.GetAsync(x => x.UserId == userId); + + if (userGalleryDocument != null) + { + var galleryImages = userGalleryDocument.GalleryOfImages.Where(g => g.ImageUrl != imageUrl).ToList(); + userGalleryDocument.GalleryOfImages = galleryImages; + await _repository.UpdateAsync(userGalleryDocument); + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserSettingsRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserSettingsRepository.cs new file mode 100644 index 000000000..a0dfd3e17 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserSettingsRepository.cs @@ -0,0 +1,63 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + [ExcludeFromCodeCoverage] + public class UserSettingsRepository : IUserSettingsRepository + { + private readonly IMongoRepository _repository; + + public UserSettingsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetUserSettingsAsync(Guid studentId) + { + var userSettingsDocument = await _repository.GetAsync(x => x.StudentId == studentId); + return userSettingsDocument?.AsEntity(); + } + + public async Task AddUserSettingsAsync(UserSettings userSettings) + { + var userSettingsDocument = userSettings.AsDocument(); + await _repository.AddAsync(userSettingsDocument); + } + + public async Task UpdateUserSettingsAsync(UserSettings userSettings) + { + var userSettingsDocument = await _repository.GetAsync(x => x.StudentId == userSettings.StudentId); + + if (userSettingsDocument == null) + { + userSettingsDocument = userSettings.AsDocument(); + await _repository.AddAsync(userSettingsDocument); + } + else + { + userSettingsDocument.AvailableSettings = new UserAvailableSettingsDocument + { + CreatedAtVisibility = userSettings.AvailableSettings.CreatedAtVisibility, + DateOfBirthVisibility = userSettings.AvailableSettings.DateOfBirthVisibility, + InterestedInEventsVisibility = userSettings.AvailableSettings.InterestedInEventsVisibility, + SignedUpEventsVisibility = userSettings.AvailableSettings.SignedUpEventsVisibility, + EducationVisibility = userSettings.AvailableSettings.EducationVisibility, + WorkPositionVisibility = userSettings.AvailableSettings.WorkPositionVisibility, + LanguagesVisibility = userSettings.AvailableSettings.LanguagesVisibility, + InterestsVisibility = userSettings.AvailableSettings.InterestsVisibility, + ContactEmailVisibility = userSettings.AvailableSettings.ContactEmailVisibility, + PhoneNumberVisibility = userSettings.AvailableSettings.PhoneNumberVisibility, + FrontendVersion = userSettings.AvailableSettings.FrontendVersion, + PreferredLanguage = userSettings.AvailableSettings.PreferredLanguage + }; + await _repository.UpdateAsync(userSettingsDocument); + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs new file mode 100644 index 000000000..029c915f2 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Students.Infrastructure.Options +{ + public class MongoDbOptions + { + public string ConnectionString { get; set; } + public string WriteDatabase { get; set; } + public string ReadDatabase { get; set; } + public bool Seed { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs index a12c9eb49..98f30eca2 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs @@ -2,7 +2,9 @@ using MiniSpace.Services.Students.Application.Services; using MiniSpace.Services.Students.Core; using MiniSpace.Services.Students.Core.Events; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; namespace MiniSpace.Services.Students.Infrastructure.Services { @@ -22,15 +24,29 @@ public IEvent Map(IDomainEvent @event) return new Application.Events.StudentUpdated( e.Student.Id, e.Student.FullName, - e.Student.ProfileImageUrl, - e.Student.BannerUrl, - e.Student.GalleryOfImageUrls, - e.Student.Education, - e.Student.WorkPosition, - e.Student.Company, - e.Student.Languages, - e.Student.Interests, - e.Student.ContactEmail); + e.Student.Description, + e.Student.Education.Select(ed => new Application.Dto.EducationDto + { + InstitutionName = ed.InstitutionName, + Degree = ed.Degree, + StartDate = ed.StartDate, + EndDate = ed.EndDate, + Description = ed.Description + }), + e.Student.Work.Select(w => new Application.Dto.WorkDto + { + Company = w.Company, + Position = w.Position, + StartDate = w.StartDate, + EndDate = w.EndDate, + Description = w.Description + }), + e.Student.Languages.Select(i => i.ToString()), + e.Student.Interests.Select(i => i.ToString()), + e.Student.ContactEmail, + e.Student.Country, + e.Student.City, + e.Student.DateOfBirth); case StudentStateChanged e: return new Application.Events.StudentStateChanged( e.Student.Id, diff --git a/MiniSpace.Web/src/MiniSpace.Web/.gitignore b/MiniSpace.Web/src/MiniSpace.Web/.gitignore new file mode 100644 index 000000000..f3a87d104 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/.gitignore @@ -0,0 +1,3 @@ +appsettings.local.json +appsettings.docker.json +appsettings.json diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs index 9d99e8c05..3938c5a29 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs @@ -34,5 +34,6 @@ public interface IIdentityService Task EnableTwoFactorAsync(Guid userId, string secret); Task DisableTwoFactorAsync(Guid userId); + Task> VerifyTwoFactorCodeAsync(Guid userId, string code); } } \ 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 e22ec9f0c..924b1cf72 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -34,6 +34,9 @@ public IdentityService(IHttpClient httpClient, ILocalStorageService localStorage public Task GetAccountAsync(JwtDto jwtDto) { + if (jwtDto == null || string.IsNullOrEmpty(jwtDto.AccessToken)) + throw new ArgumentNullException(nameof(jwtDto), "JWT DTO or Access Token is null"); + _httpClient.SetAccessToken(jwtDto.AccessToken); return _httpClient.GetAsync("identity/me"); } @@ -51,6 +54,13 @@ public async Task> SignInAsync(string email, string passwor if (response.Content != null) { JwtDto = response.Content; + + if (JwtDto.IsTwoFactorRequired) + { + // Indicate that 2FA is required + return response; + } + var jwtDtoJson = JsonSerializer.Serialize(JwtDto); await _localStorage.SetItemAsStringAsync("jwtDto", jwtDtoJson); @@ -61,6 +71,10 @@ public async Task> SignInAsync(string email, string passwor Email = payload.Claims.FirstOrDefault(c => c.Type == "e-mail")?.Value; IsAuthenticated = true; } + else + { + throw new InvalidOperationException("Failed to sign in. No JWT token received."); + } return response; } @@ -114,44 +128,47 @@ public async Task GetAccessTokenAsync() { JwtDto jwtDto = JsonSerializer.Deserialize(jwtDtoJson); - var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); - - if (jwtToken.ValidTo > DateTime.UtcNow) - { - return jwtDto.AccessToken; - } - else + if (jwtDto != null && !string.IsNullOrEmpty(jwtDto.AccessToken)) { - if (!string.IsNullOrEmpty(jwtDto.RefreshToken)) + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + + if (jwtToken.ValidTo > DateTime.UtcNow) + { + return jwtDto.AccessToken; + } + else { - try + if (!string.IsNullOrEmpty(jwtDto.RefreshToken)) { - JwtDto newJwtDto = await RefreshAccessToken(jwtDto.RefreshToken); - - if (newJwtDto != null) + try { - var newJwtDtoJson = JsonSerializer.Serialize(newJwtDto); + JwtDto newJwtDto = await RefreshAccessToken(jwtDto.RefreshToken); - await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); + if (newJwtDto != null) + { + var newJwtDtoJson = JsonSerializer.Serialize(newJwtDto); - return newJwtDto.AccessToken; + await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); + + return newJwtDto.AccessToken; + } } - } - catch (Exception ex) - { - await _localStorage.RemoveItemAsync("jwtDto"); + catch (Exception ex) + { + await _localStorage.RemoveItemAsync("jwtDto"); - _navigationManager.NavigateTo("signin", forceLoad: true); + _navigationManager.NavigateTo("signin", forceLoad: true); - throw new InvalidOperationException("Failed to refresh token: " + ex.Message); + throw new InvalidOperationException("Failed to refresh token: " + ex.Message); + } } - } - await _localStorage.RemoveItemAsync("jwtDto"); + await _localStorage.RemoveItemAsync("jwtDto"); - _navigationManager.NavigateTo("signin", forceLoad: true); + _navigationManager.NavigateTo("signin", forceLoad: true); - throw new InvalidOperationException("Session expired, please login again."); + throw new InvalidOperationException("Session expired, please login again."); + } } } @@ -190,37 +207,63 @@ public async Task CheckIfUserIsAuthenticated() if (!string.IsNullOrEmpty(jwtDtoJson)) { JwtDto jwtDto = JsonSerializer.Deserialize(jwtDtoJson); - var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); - if (jwtToken.ValidTo > DateTime.UtcNow) + if (jwtDto?.AccessToken != null) { - IsAuthenticated = true; + try + { + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + if (jwtToken.ValidTo > DateTime.UtcNow) + { + IsAuthenticated = true; + } + else + { + IsAuthenticated = await TryRefreshToken(jwtDto.RefreshToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"Token validation error: {ex.Message}"); + IsAuthenticated = false; + } } else { - IsAuthenticated = await TryRefreshToken(jwtDto.RefreshToken); + Console.WriteLine("AccessToken is null"); + IsAuthenticated = false; } } else { + Console.WriteLine("jwtDtoJson is null or empty"); IsAuthenticated = false; } return IsAuthenticated; } - + private async Task TryRefreshToken(string refreshToken) { try { - var response = await _httpClient.PostAsync("identity/refresh-tokens/use", new { refreshToken }); - if (response.Content != null) + if (!string.IsNullOrEmpty(refreshToken)) + { + var response = await _httpClient.PostAsync("identity/refresh-tokens/use", new { refreshToken }); + if (response.Content != null) + { + var newJwtDtoJson = JsonSerializer.Serialize(response.Content); + await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); + JwtDto = response.Content; + return true; + } + } + else { - var newJwtDtoJson = JsonSerializer.Serialize(response.Content); - await _localStorage.SetItemAsStringAsync("jwtDto", newJwtDtoJson); - return true; + Console.WriteLine("RefreshToken is null or empty"); } } - catch (Exception) + catch (Exception ex) { + Console.WriteLine($"Error refreshing token: {ex.Message}"); await Logout(); } return false; @@ -232,8 +275,19 @@ public async Task IsTokenValid() if (!string.IsNullOrEmpty(jwtDtoJson)) { var jwtDto = JsonSerializer.Deserialize(jwtDtoJson); - var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); - return jwtToken.ValidTo > DateTime.UtcNow; + if (jwtDto != null && !string.IsNullOrEmpty(jwtDto.AccessToken)) + { + var jwtToken = _jwtHandler.ReadJwtToken(jwtDto.AccessToken); + return jwtToken.ValidTo > DateTime.UtcNow; + } + else + { + Console.WriteLine("jwtDto or AccessToken is null"); + } + } + else + { + Console.WriteLine("jwtDtoJson is null or empty"); } return false; } @@ -335,7 +389,7 @@ public async Task GenerateTwoFactorSecretAsync(Guid userId) throw new InvalidOperationException("Failed to generate two-factor secret."); } - public async Task EnableTwoFactorAsync(Guid userId, string secret) + public async Task EnableTwoFactorAsync(Guid userId, string secret) { await _httpClient.PostAsync("identity/2fa/enable", new { UserId = userId, Secret = secret }); } @@ -345,6 +399,25 @@ public async Task DisableTwoFactorAsync(Guid userId) await _httpClient.PostAsync("identity/2fa/disable", new { UserId = userId }); } - + public async Task> VerifyTwoFactorCodeAsync(Guid userId, string code) + { + var payload = new { userId, code }; + var response = await _httpClient.PostAsync("identity/2fa/verify-code", payload); + if (response.Content != null) + { + JwtDto = response.Content; + var jwtDtoJson = JsonSerializer.Serialize(JwtDto); + await _localStorage.SetItemAsStringAsync("jwtDto", jwtDtoJson); + + var jwtToken = _jwtHandler.ReadJwtToken(JwtDto.AccessToken); + var payloadClaims = jwtToken.Payload; + UserDto = await GetAccountAsync(JwtDto); + Name = payloadClaims.Claims.FirstOrDefault(c => c.Type == "name")?.Value; + Email = payloadClaims.Claims.FirstOrDefault(c => c.Type == "e-mail")?.Value; + IsAuthenticated = true; + } + return response; + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs index 07e8483cf..f037cd191 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Http; using MiniSpace.Web.DTO; using MiniSpace.Web.HttpClients; @@ -9,8 +11,21 @@ public interface IMediaFilesService { public Task GetFileAsync(Guid fileId); public Task GetOriginalFileAsync(Guid fileId); - public Task> UploadMediaFileAsync(Guid sourceId, string sourceType, - Guid uploaderId, string fileName, string fileContentType, string base64Content); + public Task> UploadMediaFileAsync( + Guid sourceId, + string sourceType, + Guid uploaderId, + string fileName, + string fileContentType, + byte[] fileData); + + public Task> UploadOrganizationImageAsync( + Guid organizationId, + string sourceType, + Guid uploaderId, + string fileName, + string fileContentType, + byte[] fileData); public Task DeleteMediaFileAsync(string fileUrl); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs index f372748b6..85d83b9a1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs @@ -34,26 +34,53 @@ public Task> UploadMediaFileAsync( Guid uploaderId, string fileName, string fileContentType, - string base64Content) + byte[] fileData) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("media-files", new { - MediaFileId = Guid.NewGuid(), + + var requestBody = new + { + MediaFileId = Guid.NewGuid(), SourceId = sourceId, SourceType = sourceType, UploaderId = uploaderId, FileName = fileName, FileContentType = fileContentType, - Base64Content = base64Content - }); + FileData = fileData + }; + + return _httpClient.PostAsync("media-files", requestBody); } - public Task DeleteMediaFileAsync(string fileUrl) + public Task DeleteMediaFileAsync(string fileUrl) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.DeleteAsync($"media-files/delete/{Uri.EscapeDataString(fileUrl)}", new { MediaFileUrl = fileUrl }); + return _httpClient.DeleteAsync($"media-files/delete/{Uri.EscapeDataString(fileUrl)}", + new { MediaFileUrl = fileUrl }); } - + public Task> UploadOrganizationImageAsync( + Guid organizationId, + string sourceType, + Guid uploaderId, + string fileName, + string fileContentType, + byte[] fileData) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + + var requestBody = new + { + OrganizationId = organizationId, + MediaFileId = Guid.NewGuid(), + SourceType = sourceType, + UploaderId = uploaderId, + FileName = fileName, + FileContentType = fileContentType, + FileData = fileData + }; + + return _httpClient.PostAsync("media-files", requestBody); + } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/AssignRoleToMemberCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/AssignRoleToMemberCommand.cs new file mode 100644 index 000000000..e8a8d50b9 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/AssignRoleToMemberCommand.cs @@ -0,0 +1,19 @@ +using System; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class AssignRoleToMemberCommand + { + public Guid OrganizationId { get; } + public Guid MemberId { get; } + public string Role { get; } + + public AssignRoleToMemberCommand(Guid organizationId, Guid memberId, string role) + { + OrganizationId = organizationId; + MemberId = memberId; + Role = role; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationDto.cs new file mode 100644 index 000000000..f7ebb8221 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationDto.cs @@ -0,0 +1,18 @@ +using System; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class CreateOrganizationDto + { + public Guid OrganizationId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public Guid? RootId { get; set; } + public Guid? ParentId { get; set; } = null; + public Guid OwnerId { get; set; } + public OrganizationSettingsDto Settings { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationRoleCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationRoleCommand.cs new file mode 100644 index 000000000..8e7d033ed --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateOrganizationRoleCommand.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class CreateOrganizationRoleCommand + { + public Guid OrganizationId { get; } + public string RoleName { get; } + public string Description { get; } + public Dictionary Permissions { get; } + + public CreateOrganizationRoleCommand(Guid organizationId, string roleName, string description, Dictionary permissions) + { + OrganizationId = organizationId; + RoleName = roleName; + Description = description; + Permissions = permissions; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateSubOrganizationCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateSubOrganizationCommand.cs new file mode 100644 index 000000000..f217f35c5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/CreateSubOrganizationCommand.cs @@ -0,0 +1,18 @@ +using System; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class CreateSubOrganizationCommand + { + public Guid SubOrganizationId { get; } + public string Name { get; } + public string Description { get; } + public Guid RootId { get; } + public Guid ParentId { get; } + public Guid OwnerId { get; } + public OrganizationSettingsDto Settings { get; } + public string BannerUrl { get; } + public string ImageUrl { get; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/InviteUserToOrganizationCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/InviteUserToOrganizationCommand.cs new file mode 100644 index 000000000..8549f5a4a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/InviteUserToOrganizationCommand.cs @@ -0,0 +1,17 @@ +using System; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class InviteUserToOrganizationCommand + { + public Guid OrganizationId { get; } + public Guid UserId { get; } + + public InviteUserToOrganizationCommand(Guid organizationId, Guid userId) + { + OrganizationId = organizationId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/ManageFeedCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/ManageFeedCommand.cs new file mode 100644 index 000000000..f0b693b96 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/ManageFeedCommand.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class ManageFeedCommand + { + public Guid OrganizationId { get; } + public string Content { get; } + public string Action { get; } + + public ManageFeedCommand(Guid organizationId, string content, string action) + { + OrganizationId = organizationId; + Content = content; + Action = action; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationPrivacyCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationPrivacyCommand.cs new file mode 100644 index 000000000..2660d0737 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationPrivacyCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class SetOrganizationPrivacyCommand + { + public Guid OrganizationId { get; } + public bool IsPrivate { get; } + + public SetOrganizationPrivacyCommand(Guid organizationId, bool isPrivate) + { + OrganizationId = organizationId; + IsPrivate = isPrivate; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationVisibilityCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationVisibilityCommand.cs new file mode 100644 index 000000000..4415c9baa --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/SetOrganizationVisibilityCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class SetOrganizationVisibilityCommand + { + public Guid OrganizationId { get; } + public bool IsVisible { get; } + + public SetOrganizationVisibilityCommand(Guid organizationId, bool isVisible) + { + OrganizationId = organizationId; + IsVisible = isVisible; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationCommand.cs new file mode 100644 index 000000000..ac3563411 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationCommand.cs @@ -0,0 +1,35 @@ +using System; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class UpdateOrganizationCommand + { + public Guid OrganizationId { get; private set; } + public string Name { get; } + public string Description { get; } + public Guid RootId { get; } + public Guid ParentId { get; } + public Guid OwnerId { get; } + public OrganizationSettingsDto Settings { get; } + public string BannerUrl { get; } + public string ImageUrl { get; } + public string DefaultRoleName { get; } + + public UpdateOrganizationCommand(Guid organizationId, string name, string description, + Guid rootId, Guid parentId, Guid ownerId, OrganizationSettingsDto settings, + string bannerUrl, string imageUrl, string defaultRoleName) + { + OrganizationId = organizationId == Guid.Empty ? Guid.NewGuid() : organizationId; + Name = name; + Description = description; + RootId = rootId; + ParentId = parentId; + OwnerId = ownerId; + Settings = settings; + BannerUrl = bannerUrl; + ImageUrl = imageUrl; + DefaultRoleName = defaultRoleName; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationSettingsCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationSettingsCommand.cs new file mode 100644 index 000000000..6b0ec0f52 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateOrganizationSettingsCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class UpdateOrganizationSettingsCommand + { + public Guid OrganizationId { get; } + public OrganizationSettingsDto Settings { get; } + + public UpdateOrganizationSettingsCommand(Guid organizationId, OrganizationSettingsDto settings) + { + OrganizationId = organizationId; + Settings = settings; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateRolePermissionsCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateRolePermissionsCommand.cs new file mode 100644 index 000000000..689e75c57 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/CommandsDto/UpdateRolePermissionsCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Organizations; + +namespace MiniSpace.Web.Areas.Organizations.CommandsDto +{ + public class UpdateRolePermissionsCommand + { + public Guid OrganizationId { get; } + public Guid RoleId { get; } + public string RoleName { get; set; } + public string Description { get; set; } + public Dictionary Permissions { get; } + + public UpdateRolePermissionsCommand(Guid organizationId, Guid roleId, string roleName, + string description, Dictionary permissions) + { + OrganizationId = organizationId; + RoleId = roleId; + RoleName = roleName; + Description = description; + Permissions = permissions; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs index ac843b9b7..f7b8148ab 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs @@ -1,25 +1,33 @@ 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.Areas.Organizations.CommandsDto; +using MiniSpace.Web.DTO.Organizations; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Organizations { public interface IOrganizationsService { - Task GetOrganizationAsync(Guid organizationId, Guid rootId); - Task GetOrganizationDetailsAsync(Guid organizationId, Guid rootId); - Task> GetOrganizerOrganizationsAsync(Guid organizerId); + Task GetOrganizationAsync(Guid organizationId); + Task GetOrganizationDetailsAsync(Guid organizationId); Task> GetRootOrganizationsAsync(); - Task> GetChildrenOrganizationsAsync(Guid organizationId, Guid rootId); - Task> GetAllChildrenOrganizationsAsync(Guid organizationId, Guid rootId); - Task> CreateOrganizationAsync(Guid organizationId, string name, Guid rootId, Guid parentId); - Task> CreateRootOrganizationAsync(Guid organizationId, string name); - Task DeleteOrganizationAsync(Guid organizationId, Guid rootId); - Task AddOrganizerToOrganizationAsync(Guid rootOrganizationId, Guid organizationId, Guid organizerId); - Task RemoveOrganizerFromOrganizationAsync(Guid rootOrganizationId, Guid organizationId, Guid organizerId); - } + Task> GetChildrenOrganizationsAsync(Guid organizationId); + Task GetOrganizationWithGalleryAndUsersAsync(Guid organizationId); + Task> GetAllChildrenOrganizationsAsync(Guid organizationId); + Task> CreateOrganizationAsync(CreateOrganizationDto command); + Task> CreateSubOrganizationAsync(Guid parentOrganizationId, CreateSubOrganizationCommand command); + Task DeleteOrganizationAsync(Guid organizationId); + Task> CreateOrganizationRoleAsync(Guid organizationId, CreateOrganizationRoleCommand command); + Task InviteUserToOrganizationAsync(Guid organizationId, InviteUserToOrganizationCommand command); + Task AssignRoleToMemberAsync(Guid organizationId, Guid memberId, AssignRoleToMemberCommand command); + Task> UpdateRolePermissionsAsync(Guid organizationId, Guid roleId, UpdateRolePermissionsCommand command); + Task SetOrganizationPrivacyAsync(Guid organizationId, SetOrganizationPrivacyCommand command); + Task> UpdateOrganizationSettingsAsync(Guid organizationId, UpdateOrganizationSettingsCommand command); + Task SetOrganizationVisibilityAsync(Guid organizationId, SetOrganizationVisibilityCommand command); + Task ManageFeedAsync(Guid organizationId, ManageFeedCommand command); + Task> UpdateOrganizationAsync(Guid organizationId, UpdateOrganizationCommand command); + Task> GetUserOrganizationsAsync(Guid userId); + Task> GetOrganizationRolesAsync(Guid organizationId); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs index 9da60a2d4..476689d61 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; -using MiniSpace.Web.DTO; +using MiniSpace.Web.Areas.Organizations.CommandsDto; +using MiniSpace.Web.DTO.Organizations; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Organizations @@ -17,75 +18,125 @@ public OrganizationsService(IHttpClient httpClient, IIdentityService identitySer _httpClient = httpClient; _identityService = identityService; } - - public Task GetOrganizationAsync(Guid organizationId, Guid rootId) + + public Task GetOrganizationAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync($"organizations/{organizationId}?rootId={rootId}"); + return _httpClient.GetAsync($"organizations/{organizationId}"); } - public Task GetOrganizationDetailsAsync(Guid organizationId, Guid rootId) + public Task GetOrganizationDetailsAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync($"organizations/{organizationId}/details?rootId={rootId}"); + return _httpClient.GetAsync($"organizations/{organizationId}/details"); } - public Task> GetOrganizerOrganizationsAsync(Guid organizerId) + public Task> GetRootOrganizationsAsync() { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"organizations/organizer/{organizerId}"); + return _httpClient.GetAsync>("organizations/root"); } - public Task> GetRootOrganizationsAsync() + public Task> GetChildrenOrganizationsAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>("organizations/root"); + return _httpClient.GetAsync>($"organizations/{organizationId}/children"); } - public Task> GetChildrenOrganizationsAsync(Guid organizationId, Guid rootId) + public Task> GetAllChildrenOrganizationsAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync> - ($"organizations/{organizationId}/children?rootId={rootId}"); + return _httpClient.GetAsync>($"organizations/{organizationId}/children/all"); } - public Task> GetAllChildrenOrganizationsAsync(Guid organizationId, Guid rootId) + public Task GetOrganizationWithGalleryAndUsersAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync> - ($"organizations/{organizationId}/children/all?rootId={rootId}"); + return _httpClient.GetAsync($"organizations/{organizationId}/details/gallery-users"); } - - public Task> CreateOrganizationAsync(Guid organizationId, string name, Guid rootId, Guid parentId) + + public Task> CreateOrganizationAsync(CreateOrganizationDto command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync($"organizations/{organizationId}/children", - new {organizationId, name, rootId, parentId}); + return _httpClient.PostAsync("organizations", command); } - public Task> CreateRootOrganizationAsync(Guid organizationId, string name) + public Task> CreateSubOrganizationAsync(Guid parentOrganizationId, CreateSubOrganizationCommand command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("organizations", new {organizationId, name}); + return _httpClient.PostAsync($"organizations/{parentOrganizationId}/children", command); } - - public Task DeleteOrganizationAsync(Guid organizationId, Guid rootId) + + public Task DeleteOrganizationAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.DeleteAsync($"organizations/{organizationId}?rootId={rootId}"); + return _httpClient.DeleteAsync($"organizations/{organizationId}"); } - - public Task AddOrganizerToOrganizationAsync(Guid rootOrganizationId, Guid organizationId, Guid organizerId) + + public Task> CreateOrganizationRoleAsync(Guid organizationId, CreateOrganizationRoleCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/roles", command); + } + + public Task InviteUserToOrganizationAsync(Guid organizationId, InviteUserToOrganizationCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/invite", command); + } + + public Task AssignRoleToMemberAsync(Guid organizationId, Guid memberId, AssignRoleToMemberCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/roles/{memberId}", command); + } + + public Task> UpdateRolePermissionsAsync(Guid organizationId, Guid roleId, UpdateRolePermissionsCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"organizations/{organizationId}/roles/{roleId}/permissions", command); + } + + public Task SetOrganizationPrivacyAsync(Guid organizationId, SetOrganizationPrivacyCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"organizations/{organizationId}/privacy", command); + } + + public Task> UpdateOrganizationSettingsAsync(Guid organizationId, UpdateOrganizationSettingsCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"organizations/{organizationId}/settings", command); + } + + public Task SetOrganizationVisibilityAsync(Guid organizationId, SetOrganizationVisibilityCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"organizations/{organizationId}/visibility", command); + } + + public Task ManageFeedAsync(Guid organizationId, ManageFeedCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"organizations/{organizationId}/feed", command); + } + + public Task> UpdateOrganizationAsync(Guid organizationId, UpdateOrganizationCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"organizations/{organizationId}", command); + } + + public Task> GetUserOrganizationsAsync(Guid userId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync($"organizations/{organizationId}/organizer", - new {rootOrganizationId, organizationId, organizerId}); + return _httpClient.GetAsync>($"organizations/users/{userId}/organizations"); } - public Task RemoveOrganizerFromOrganizationAsync(Guid rootOrganizationId, Guid organizationId, Guid organizerId) + public Task> GetOrganizationRolesAsync(Guid organizationId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.DeleteAsync($"organizations/{organizationId}/organizer/{organizerId}?rootOrganizationId={rootOrganizationId}"); + return _httpClient.GetAsync>($"organizations/{organizationId}/roles"); } - } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs index abcee86e8..cf5105ca8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Interests; +using MiniSpace.Web.DTO.Languages; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -26,14 +28,25 @@ Task UpdateStudentAsync( bool enableTwoFactor, bool disableTwoFactor, string twoFactorSecret, - string education, - string workPosition, - string company); + IEnumerable education, + IEnumerable work, + string phoneNumber, + string country, + string city, + DateTime? dateOfBirth); public Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImageUrl, string description, DateTime dateOfBirth, bool emailNotifications, string contactEmail); Task GetStudentStateAsync(Guid studentId); Task GetUserNotificationPreferencesAsync(Guid studentId); - Task UpdateUserNotificationPreferencesAsync(Guid studentId, NotificationPreferencesDto preferencesDto); + Task UpdateUserNotificationPreferencesAsync(Guid studentId, NotificationPreferencesDto preferencesDto, bool emailNotifications); + Task GetStudentWithGalleryImagesAsync(Guid studentId); + Task UpdateUserSettingsAsync(Guid studentId, AvailableSettingsDto availableSettings); + Task GetUserSettingsAsync(Guid studentId); + Task UpdateStudentLanguagesAndInterestsAsync( + Guid studentId, + IEnumerable languages, + IEnumerable interests); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index be56f2196..79c066e8d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Interests; +using MiniSpace.Web.DTO.Languages; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -46,7 +50,7 @@ public Task> GetStudentsAsync() return _httpClient.GetAsync>("students"); } - public async Task UpdateStudentAsync( + public async Task UpdateStudentAsync( Guid studentId, string firstName, string lastName, @@ -59,9 +63,12 @@ public async Task UpdateStudentAsync( bool enableTwoFactor, bool disableTwoFactor, string twoFactorSecret, - string education, - string workPosition, - string company) + IEnumerable education, + IEnumerable work, + string phoneNumber, + string country, + string city, + DateTime? dateOfBirth) { var accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); @@ -75,14 +82,17 @@ public async Task UpdateStudentAsync( description, emailNotifications, contactEmail, - languages, - interests, + languages = languages.ToList(), + interests = interests.ToList(), enableTwoFactor, disableTwoFactor, twoFactorSecret, education, - workPosition, - company + work, + phoneNumber, + country, + city, + dateOfBirth }; var jsonData = JsonSerializer.Serialize(updateStudentData); @@ -91,6 +101,13 @@ public async Task UpdateStudentAsync( await _httpClient.PutAsync($"students/{studentId}", updateStudentData); } + public async Task GetUserNotificationPreferencesAsync(Guid studentId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{studentId}/notifications"); + } + public Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImageUrl, string description, DateTime dateOfBirth, bool emailNotifications, string contactEmail) => _httpClient.PostAsync("students", new { studentId, profileImageUrl, description, dateOfBirth, emailNotifications, contactEmail }); @@ -100,15 +117,7 @@ public async Task GetStudentStateAsync(Guid studentId) return student != null ? student.State : "invalid"; } - // New methods for notification preferences - public async Task GetUserNotificationPreferencesAsync(Guid studentId) - { - var accessToken = await _identityService.GetAccessTokenAsync(); - _httpClient.SetAccessToken(accessToken); - return await _httpClient.GetAsync($"students/{studentId}/notifications"); - } - - public async Task UpdateUserNotificationPreferencesAsync(Guid studentId, NotificationPreferencesDto preferencesDto) + public async Task UpdateUserNotificationPreferencesAsync(Guid studentId, NotificationPreferencesDto preferencesDto, bool emailNotifications) { var accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); @@ -116,6 +125,7 @@ public async Task UpdateUserNotificationPreferencesAsync(Guid studentId, Notific var updatePreferencesData = new { studentId, + emailNotifications, preferencesDto.AccountChanges, preferencesDto.SystemLogin, preferencesDto.NewEvent, @@ -126,11 +136,69 @@ public async Task UpdateUserNotificationPreferencesAsync(Guid studentId, Notific preferencesDto.FriendsNotifications }; - // Serialize the data to JSON and log it var jsonData = JsonSerializer.Serialize(updatePreferencesData); Console.WriteLine($"Sending UpdateUserNotificationPreferences request: {jsonData}"); await _httpClient.PostAsync($"students/{studentId}/notifications", updatePreferencesData); } + + public async Task GetStudentWithGalleryImagesAsync(Guid studentId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{studentId}/gallery"); + } + + public async Task UpdateUserSettingsAsync(Guid studentId, AvailableSettingsDto availableSettings) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var updateUserSettingsData = new + { + studentId, + CreatedAtVisibility = availableSettings.CreatedAtVisibility.ToString(), + DateOfBirthVisibility = availableSettings.DateOfBirthVisibility.ToString(), + InterestedInEventsVisibility = availableSettings.InterestedInEventsVisibility.ToString(), + SignedUpEventsVisibility = availableSettings.SignedUpEventsVisibility.ToString(), + EducationVisibility = availableSettings.EducationVisibility.ToString(), + WorkPositionVisibility = availableSettings.WorkPositionVisibility.ToString(), + LanguagesVisibility = availableSettings.LanguagesVisibility.ToString(), + InterestsVisibility = availableSettings.InterestsVisibility.ToString(), + ContactEmailVisibility = availableSettings.ContactEmailVisibility.ToString(), + PhoneNumberVisibility = availableSettings.PhoneNumberVisibility.ToString(), + PreferredLanguage = availableSettings.PreferredLanguage.ToString(), + FrontendVersion = availableSettings.FrontendVersion.ToString() + }; + + await _httpClient.PutAsync($"students/{studentId}/settings", updateUserSettingsData); + } + + public async Task GetUserSettingsAsync(Guid studentId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + return await _httpClient.GetAsync($"students/{studentId}/settings"); + } + + public async Task UpdateStudentLanguagesAndInterestsAsync( + Guid studentId, + IEnumerable languages, + IEnumerable interests) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var updateData = new + { + languages = languages.ToList(), + interests = interests.ToList() + }; + + var jsonData = JsonSerializer.Serialize(updateData); + Console.WriteLine($"Sending UpdateStudentLanguagesAndInterests request: {jsonData}"); + + await _httpClient.PutAsync($"students/{studentId}/languages-and-interests", updateData); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/AvailableSettingsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/AvailableSettingsDto.cs new file mode 100644 index 000000000..08545aa47 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/AvailableSettingsDto.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Web.DTO +{ + public class AvailableSettingsDto + { + public Visibility CreatedAtVisibility { get; set; } + public Visibility DateOfBirthVisibility { get; set; } + public Visibility InterestedInEventsVisibility { get; set; } + public Visibility SignedUpEventsVisibility { get; set; } + public Visibility EducationVisibility { get; set; } + public Visibility WorkPositionVisibility { get; set; } + public Visibility LanguagesVisibility { get; set; } + public Visibility InterestsVisibility { get; set; } + public Visibility ContactEmailVisibility { get; set; } + public Visibility PhoneNumberVisibility { get; set; } + public PreferredLanguage PreferredLanguage { get; set; } + public FrontendVersion FrontendVersion { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/EducationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/EducationDto.cs new file mode 100644 index 000000000..94a33ff83 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/EducationDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class EducationDto + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs index bcbc2215f..97d0cb1e6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/NotificationEventType.cs @@ -29,6 +29,8 @@ public enum NotificationEventType ReportResolved, ReportReviewStarted, ReportCancelled, - Other + Other, + EmailVerified, + TwoFactorCodeGenerated, } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Permission.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Permission.cs new file mode 100644 index 000000000..2fd60a7cf --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Permission.cs @@ -0,0 +1,33 @@ +namespace MiniSpace.Web.DTO.Enums +{ + public enum Permission + { + CreateSubGroups = 0, + DeleteSubGroups = 1, + EditOrganizationDetails = 2, + InviteUsers = 3, + RemoveMembers = 4, + ManageMembershipRequests = 5, + MakePosts = 6, + EditPosts = 7, + DeletePosts = 8, + CommentOnPosts = 9, + DeleteComments = 10, + MakeReactions = 11, + PinPosts = 12, + CreateEvents = 13, + EditEvents = 14, + DeleteEvents = 15, + ManageEventParticipation = 16, + AssignRoles = 17, + EditPermissions = 18, + ViewAuditLogs = 19, + SendMessageToAll = 20, + CreateCommunicationChannels = 21, + ManageFeed = 22, + ModerateContent = 23, + ModifyGallery = 24, + UpdateProfileImage = 25, + UpdateOrganizationImage = 26 + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FrontendVersion.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FrontendVersion.cs new file mode 100644 index 000000000..aa5cde2fa --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FrontendVersion.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Web.DTO +{ + public enum FrontendVersion + { + Auto, + DarkMode, + LightMode, + SystemMode, + Default + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/GalleryImageDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/GalleryImageDto.cs new file mode 100644 index 000000000..c9ea66224 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/GalleryImageDto.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class GalleryImageDto + { + public Guid ImageId { get; set; } + public string ImageUrl { get; set; } + public DateTime DateAdded { get; set; } + + public GalleryImageDto(Guid imageId, string imageUrl, DateTime dateAdded) + { + ImageId = imageId; + ImageUrl = !string.IsNullOrWhiteSpace(imageUrl) ? imageUrl : "/images/default_image.png"; + DateAdded = dateAdded; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/InterestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/InterestDto.cs new file mode 100644 index 000000000..02ca898ce --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/InterestDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Web.DTO +{ + public class InterestDto + { + public string Name { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Interests/Interest.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Interests/Interest.cs new file mode 100644 index 000000000..3cbdfd4ea --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Interests/Interest.cs @@ -0,0 +1,482 @@ +namespace MiniSpace.Web.DTO.Interests +{ + public enum Interest + { + Acting, + Airsoft, + Archery, + Astronomy, + Automotive, + Backpacking, + Badminton, + Baking, + Ballet, + Barbecue, + Basketball, + BeachVolleyball, + BirdWatching, + Blogging, + BoardGames, + BookClubs, + Bowling, + Boxing, + BrewingBeer, + BungeeJumping, + Calligraphy, + Camping, + Canoeing, + CardGames, + Chess, + Coding, + Collecting, + Comedy, + ComicBooks, + Cooking, + Crafting, + Cricket, + Crossfit, + Cycling, + Dancing, + Debate, + Darts, + DiningOut, + DIYProjects, + Drawing, + Embroidery, + Esports, + FantasySports, + Fashion, + FilmMaking, + Fishing, + Fitness, + FloralArranging, + FlyingDrones, + Football, + Gardening, + Genealogy, + Geocaching, + GlassBlowing, + Golf, + GourmetFood, + Gymnastics, + Hiking, + History, + Hockey, + HomeBrewing, + HorseRiding, + Hunting, + IceSkating, + Improv, + Investing, + JewelryMaking, + Juggling, + Kayaking, + Kitesurfing, + Knitting, + Lacrosse, + Landscaping, + LaserTag, + LegoBuilding, + Magic, + MartialArts, + Meditation, + MetalDetecting, + ModelBuilding, + MountainBiking, + Movies, + Music, + Networking, + Origami, + Painting, + Parkour, + PetCare, + Philately, + Philosophy, + Photography, + Piano, + Pilates, + PingPong, + Podcasts, + Poetry, + Pottery, + Puzzles, + Quilting, + Racing, + Rafting, + Reading, + RealEstate, + Robotics, + RockClimbing, + Rowing, + Rugby, + Running, + Sailing, + ScubaDiving, + Sewing, + Singing, + Skateboarding, + Skiing, + SkyDiving, + Snowboarding, + SoapMaking, + Soccer, + Spirituality, + StandUpComedy, + Surfing, + Swimming, + TableTennis, + Taekwondo, + TaiChi, + Tennis, + Theater, + Travel, + Trekking, + Trivia, + VideoGames, + Volleyball, + Volunteering, + Walking, + WatchingMovies, + WatchingSports, + WaterPolo, + Weaving, + WeightLifting, + WineTasting, + WoodWorking, + Writing, + Yoga, + Zumba, + ArcheryTag, + Astrophotography, + AutoRacing, + BadmintonSingles, + Beachcombing, + BirdPhotography, + BoardgameDesign, + Bodybuilding, + BookWriting, + BowlingTeams, + CampingHiking, + CanoeCamping, + CarShows, + CheeseMaking, + ChessTournaments, + ClaySculpting, + Climbing, + ComputerProgramming, + Concerts, + CraftBeer, + CreativeWriting, + Crochet, + DanceTeams, + DiningExperiences, + DIYHomeImprovement, + DollCollecting, + DroneRacing, + EnduranceSports, + EventPlanning, + Exercise, + FantasyFootball, + FineDining, + FloralDesign, + FoodTasting, + GardeningVegetables, + Gemology, + GhostHunting, + GlassArt, + GolfCourses, + GourmetCooking, + GymWorkouts, + Handball, + HistoricReenactments, + HomeRenovation, + HotAirBallooning, + IceFishing, + IndoorClimbing, + JewelryDesign, + JigsawPuzzles, + Judo, + KayakFishing, + KiteFlying, + KnittingClubs, + LeatherWorking, + LegoCompetitions, + Lighthouses, + MagicShows, + MakeupArtistry, + MartialArtsTournaments, + MeditationRetreats, + MiniatureBuilding, + ModelAircraft, + ModelRailroading, + MountainClimbing, + MovieProduction, + MusicBands, + NatureHiking, + OrigamiArt, + PaintingClasses, + Paragliding, + ParkPhotography, + PetShows, + Philanthropy, + PhilatelyClubs, + PhilosophyGroups, + PianoConcerts, + PilatesClasses, + Poker, + PoliticalDebates, + PotteryClasses, + Powerlifting, + PuzzleChallenges, + QuiltingGroups, + RacketSports, + RaftingTrips, + ReadingCircles, + RealEstateInvesting, + Reenactments, + RoboticsClubs, + RockCollecting, + RunningMarathons, + SailboatRacing, + ScavengerHunts, + SculptureArt, + SkateParks, + SkiMountaineering, + SkyPhotography, + SoapBoxRacing, + Softball, + SpeedSkating, + SpiritualRetreats, + SportsAnalytics, + SportsPhotography, + Storytelling, + SurfPhotography, + SurfFishing, + SurvivalSkills, + TableFootball, + TaiChiClasses, + TalentShows, + Technology, + TennisMatches, + TheaterProduction, + TheatricalMakeup, + TravelPhotography, + TreeClimbing, + TriviaNights, + TropicalFish, + UrbanExploring, + VideoProduction, + VirtualReality, + VolunteerFirefighting, + Wakeboarding, + WaterAerobics, + WaterSkiing, + WeightTraining, + WineMaking, + WoodCarving, + WordGames, + Wrestling, + WritingPoetry, + YogaClasses, + ZipLining, + Zookeeping, + AirRifle, + AnimalRescue, + Aquascaping, + ArtGalleries, + BakingClasses, + BallroomDancing, + BaristaSkills, + Beekeeping, + BikeRacing, + Blacksmithing, + BoatBuilding, + BookRestoration, + BonsaiGrowing, + BrewingCider, + BullRiding, + ButterflyWatching, + CalligraphyArt, + CandleMaking, + CardTricks, + CaveExploring, + CelticMusic, + ChainsawCarving, + CircusArts, + ClassicCars, + ClayPigeonShooting, + Clowning, + ComicCollecting, + CompetitiveEating, + Cosplaying, + CountryDancing, + CraftFairs, + CricketUmpiring, + CrossCountrySkiing, + Cupping, + CurlingTeams, + DanceFitness, + DanceHall, + Design, + DogTraining, + Dominoes, + DragonBoatRacing, + ElectricVehicles, + Electronics, + FireDancing, + FirePoi, + FleaMarkets, + FolkDancing, + FoodStalls, + Forestry, + GameDesign, + GemCutting, + GeocachingHikes, + GiantPuppets, + GlassEtching, + Golfing, + GourmetCatering, + GreenEnergy, + Guitar, + Hackathons, + HandmadeCards, + HerbGardening, + HighlandGames, + HistoricalTours, + HomeAutomation, + Horticulture, + IceDiving, + InlineHockey, + InstrumentMaking, + JewelryMakingClasses, + KettlebellTraining, + KiteSurfing, + LandArt, + LaserShows, + Leathercraft, + LightArt, + LiveActionRolePlaying, + Macrame, + MarathonRunning, + MarineBiology, + MetalArt, + Microscopy, + MineralCollecting, + MixedMartialArts, + MockTrials, + ModelShips, + Monologues, + MosaicArt, + MountainRunning, + MuseumTours, + MushroomHunting, + NatureConservation, + NeedleFelting, + Needlepoint, + NightPhotography, + OrienteeringSports, + OutdoorCooking, + Paddleboarding, + PaperMaking, + ParkourClasses, + PerfumeMaking, + PetTraining, + PhilosophyLectures, + PhotographyClasses, + PianoLessons, + Pinball, + PinballMachines, + PlayingDrums, + PlayingGuitar, + PlayingSaxophone, + PlayingViolin, + PoleDancing, + PotluckDinners, + Powerboating, + PrecisionShooting, + PublicSpeaking, + Puppetry, + Quilling, + RallyDriving, + RCBoats, + RCPlanes, + RCTrucks, + RockMusic, + Rockhounding, + Rocketry, + RollerHockey, + RowingClubs, + RubiksCube, + RunningClubs, + Sailboard, + SalsaDancing, + SandSculpting, + ScienceFairs, + ScrapMetalArt, + Scrying, + SecretSanta, + SewingClasses, + Shogi, + ShrubSculpting, + SkyWatching, + SlotCars, + SlowFood, + SoapSculpting, + SoftballLeagues, + SolarEnergy, + Spelunking, + SpokenWord, + SportsScience, + SquashMatches, + StainedGlass, + StandUpPaddleboarding, + Stargazing, + Steampunk, + StoneSkipping, + StreetDancing, + StreetPerforming, + SunPhotography, + SurfKayaking, + SwingDancing, + SwordFighting, + TableHockey, + TableSoccer, + TangoDancing, + TattooArt, + TeamSports, + TechnicalWriting, + TextileArt, + TheaterActing, + TheatreHistory, + ThrowingDarts, + TimeCapsules, + ToyCollecting, + TraditionalGames, + TrampoliningTeams, + Trainspotting, + Triathlons, + TroutFishing, + TruckRacing, + UrbanGardening, + VideoBlogging, + VintageCars, + Volcanology, + WakeSurfing, + WaterBalloonFight, + Watercolors, + WheelchairBasketball, + Whittling, + WildflowerPhotography, + WildlifePhotography, + Windsurfing, + Woodburning, + WordSearch, + WorldBuilding, + WritingFiction, + WritingNonfiction, + WritingWorkshops, + Yachting, + YogaRetreats, + ZumbaFitness + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs index dd5b69029..f26940b2e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs @@ -1,3 +1,5 @@ +using System; + namespace MiniSpace.Web.DTO { public class JwtDto @@ -6,5 +8,7 @@ public class JwtDto public string Role { get; set; } public string RefreshToken { get; set; } public long Expires { get; set; } + public bool IsTwoFactorRequired { get; set; } + public Guid UserId { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Languages/Language.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Languages/Language.cs new file mode 100644 index 000000000..86d0916b1 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Languages/Language.cs @@ -0,0 +1,52 @@ +namespace MiniSpace.Web.DTO.Languages +{ + public enum Language + { + English, + Spanish, + French, + German, + Chinese, + Japanese, + Korean, + Italian, + Russian, + Portuguese, + Arabic, + Hindi, + Bengali, + Punjabi, + Javanese, + Vietnamese, + Telugu, + Marathi, + Tamil, + Urdu, + Turkish, + Persian, + Gujarati, + Polish, + Ukrainian, + Dutch, + Greek, + Czech, + Swedish, + Hungarian, + Danish, + Finnish, + Norwegian, + Hebrew, + Malay, + Indonesian, + Thai, + Filipino, + Swahili, + Zulu, + Xhosa, + Yoruba, + Igbo, + Amharic, + Somali, + Hausa + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs deleted file mode 100644 index e8afa161b..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDetailsDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 RootId { 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 deleted file mode 100644 index 8526582fa..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/OrganizationDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace MiniSpace.Web.DTO -{ - public class OrganizationDto - { - public Guid Id { get; set; } - public string Name { get; set; } - public Guid RootId { get; set; } - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/InvitationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/InvitationDto.cs new file mode 100644 index 000000000..cc77893a5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/InvitationDto.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class InvitationDto + { + public Guid UserId { get; set; } + + public InvitationDto() + { + + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDetailsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDetailsDto.cs new file mode 100644 index 000000000..187ee2ac2 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDetailsDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationDetailsDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + public Guid? ParentOrganizationId { get; set; } + public IEnumerable SubOrganizations { get; set; } = new List(); + public IEnumerable Invitations { get; set; } = new List(); + public IEnumerable Users { get; set; } = new List(); + public IEnumerable Roles { get; set; } = new List(); + public IEnumerable Gallery { get; set; } = new List(); + public OrganizationSettingsDto Settings { get; set; } + public string DefaultRoleName { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs new file mode 100644 index 000000000..bdc2d8191 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + public Guid? RootId { get; set; } + public string DefaultRoleName { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryDto.cs new file mode 100644 index 000000000..d391aa9d5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace MiniSpace.Web.DTO.Organizations +{ + [ExcludeFromCodeCoverage] + public class OrganizationGalleryDto + { + public OrganizationDto Organization { get; set; } + public IEnumerable Gallery { get; set; } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryUsersDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryUsersDto.cs new file mode 100644 index 000000000..4e3feec47 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationGalleryUsersDto.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationGalleryUsersDto + { + public OrganizationDetailsDto OrganizationDetails { get; set; } + public IEnumerable Gallery { get; set; } = new List(); + public IEnumerable Users { get; set; } = new List(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationSettingsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationSettingsDto.cs new file mode 100644 index 000000000..fa40bb334 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationSettingsDto.cs @@ -0,0 +1,19 @@ +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationSettingsDto + { + public bool IsVisible { get; set; } + public bool IsPublic { get; set; } + public bool IsPrivate { get; set; } + public bool CanAddComments { get; set; } + public bool CanAddReactions { get; set; } + public bool CanPostPosts { get; set; } + public bool CanPostEvents { get; set; } + public bool CanMakeReposts { get; set; } + public bool CanAddCommentsToPosts { get; set; } + public bool CanAddReactionsToPosts { get; set; } + public bool CanAddCommentsToEvents { get; set; } + public bool CanAddReactionsToEvents { get; set; } + public bool DisplayFeedInMainOrganization { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUserDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUserDto.cs new file mode 100644 index 000000000..50326a4f4 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUserDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationUserDto + { + public Guid Id { get; set; } + public RoleDto Role { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUsersDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUsersDto.cs new file mode 100644 index 000000000..7a18382ab --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationUsersDto.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Organizations +{ + + public class OrganizationUsersDto + { + public OrganizationDto Organization { get; set; } + public IEnumerable Users { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/RoleDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/RoleDto.cs new file mode 100644 index 000000000..a1ddcc16d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/RoleDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Web.DTO.Enums; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class RoleDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public Dictionary Permissions { get; set; } + + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/SubOrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/SubOrganizationDto.cs new file mode 100644 index 000000000..91fd7409b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/SubOrganizationDto.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class SubOrganizationDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string BannerUrl { get; set; } + public string ImageUrl { get; set; } + public Guid OwnerId { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/PreferredLanguage.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/PreferredLanguage.cs new file mode 100644 index 000000000..bf978c129 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/PreferredLanguage.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Web.DTO +{ + public enum PreferredLanguage + { + English, + Polish, + Ukrainian, + Spanish, + French, + German, + Chinese, + Japanese, + Russian, + Other + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs index a33a0b2c7..52326c689 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs @@ -1,37 +1,38 @@ using System; using System.Collections.Generic; +using MiniSpace.Web.DTO.Languages; +using MiniSpace.Web.DTO.Interests; namespace MiniSpace.Web.DTO { - public class StudentDto + public class StudentDto { public Guid Id { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public string FullName => $"{FirstName} {LastName}"; - public int NumberOfFriends { get; set; } public string ProfileImageUrl { get; set; } public string Description { get; set; } - public DateTime DateOfBirth { 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 string Education { get; set; } - public string WorkPosition { get; set; } - public string Company { get; set; } - public IEnumerable Languages { get; set; } - public IEnumerable Interests { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public List Languages { get; set; } + public List Interests { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } public IEnumerable InterestedInEvents { get; set; } public IEnumerable SignedUpEvents { get; set; } - public string BannerUrl { get; set; } - public IEnumerable GalleryOfImageUrls { get; set; } - public string ContactEmail { get; set; } - + public List GalleryOfImageUrls { get; set; } + public string Country { get; set; } + public string City { get; set; } + public bool IsInvitationPending { get; set; } public bool InvitationSent { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentWithGalleryImagesDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentWithGalleryImagesDto.cs new file mode 100644 index 000000000..022d06859 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentWithGalleryImagesDto.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Web.DTO +{ + public class StudentWithGalleryImagesDto + { + public StudentDto Student { get; set; } + public List GalleryImages { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Visibility.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Visibility.cs new file mode 100644 index 000000000..213fa3c90 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Visibility.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Web.DTO +{ + public enum Visibility + { + Everyone, + Connections, + NoOne + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/WorkDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/WorkDto.cs new file mode 100644 index 000000000..8675adaf6 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/WorkDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class WorkDto + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs index c6f01fa8f..230f1bef3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs @@ -40,7 +40,10 @@ public async Task GetAsync(string uri) { var (success, content) = await TryExecuteAsync(uri, client => client.GetAsync(uri)); - return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); + if (!success) + return default; + + return JsonConvert.DeserializeObject(content, JsonSerializerSettings); } public Task PostAsync(string uri, T request) @@ -57,8 +60,14 @@ public async Task> PostAsync(string uri var (success, content) = await TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); - return !success ? new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)) - : new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)); + if (!success) + { + var errorMessage = JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return new HttpResponse(errorMessage); + } + + var result = JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return new HttpResponse(result); } public Task PutAsync(string uri, T request) @@ -68,8 +77,14 @@ public async Task> PutAsync(string uri, { var (success, content) = await TryExecuteAsync(uri, client => client.PutAsync(uri, GetPayload(request))); - return !success ? new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)) - : new HttpResponse(JsonConvert.DeserializeObject(content, JsonSerializerSettings)); + if (!success) + { + var errorMessage = JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return new HttpResponse(errorMessage); + } + + var result = JsonConvert.DeserializeObject(content, JsonSerializerSettings); + return new HttpResponse(result); } public Task DeleteAsync(string uri) @@ -94,8 +109,6 @@ public async Task DeleteAsync(string uri, object payload) } } - - private static StringContent GetPayload(T request) { var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); @@ -103,53 +116,46 @@ private static StringContent GetPayload(T request) return new StringContent(json, Encoding.UTF8, "text/plain"); } - - - private Task<(bool success, string content)> TryExecuteAsync(string uri, Func> client) - => Policy.Handle() - .WaitAndRetryAsync(_options.Retries, r => TimeSpan.FromSeconds(Math.Pow(2, r))) - .ExecuteAsync(async () => - { - if (_client.BaseAddress != null && !Uri.IsWellFormedUriString(uri, UriKind.Absolute)) - { - if (!uri.StartsWith("/")) uri = "/" + uri; - - uri = new Uri(_client.BaseAddress, uri).ToString(); - } - else if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute)) - { - _logger.LogError($"The provided URI '{uri}' is not a valid absolute URL and no BaseAddress is set."); - return default; - } - - _logger.LogDebug($"Sending HTTP request to URI: {uri}"); - using (var response = await client(_client)) - { - if (response.IsSuccessStatusCode) - { - _logger.LogDebug($"Received a valid response to HTTP request from URI: {uri}" + - $"{Environment.NewLine}{response}"); - - return (true, await response.Content.ReadAsStringAsync()); - } - - var errorContent = "invalid_http_response"; - if (!response.IsSuccessStatusCode) + private Task<(bool success, string content)> TryExecuteAsync(string uri, Func> client) + => Policy.Handle() + .WaitAndRetryAsync(_options.Retries, r => TimeSpan.FromSeconds(Math.Pow(2, r))) + .ExecuteAsync(async () => { - 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 (false, errorContent); - } - }); - - - - - + if (_client.BaseAddress != null && !Uri.IsWellFormedUriString(uri, UriKind.Absolute)) + { + if (!uri.StartsWith("/")) uri = "/" + uri; + + uri = new Uri(_client.BaseAddress, uri).ToString(); + } + else if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute)) + { + _logger.LogError($"The provided URI '{uri}' is not a valid absolute URL and no BaseAddress is set."); + return default; + } + + _logger.LogDebug($"Sending HTTP request to URI: {uri}"); + using (var response = await client(_client)) + { + if (response.IsSuccessStatusCode) + { + _logger.LogDebug($"Received a valid response to HTTP request from URI: {uri}" + + $"{Environment.NewLine}{response}"); + + return (true, await response.Content.ReadAsStringAsync()); + } + + var errorContent = "invalid_http_response"; + if (!response.IsSuccessStatusCode) + { + 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 (false, errorContent); + } + }); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs index b6121759b..208eb0aec 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/HttpResponse.cs @@ -4,15 +4,18 @@ public class HttpResponse { public T Content { get; set; } public ErrorMessage ErrorMessage { get; set; } - + public bool IsSuccessStatusCode { get; set; } + public HttpResponse(T content) { Content = content; + IsSuccessStatusCode = true; } public HttpResponse(ErrorMessage errorMessage) { ErrorMessage = errorMessage; + IsSuccessStatusCode = false; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs index f37f70062..7f93e6770 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Organizations; namespace MiniSpace.Web.Models.Events { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/TwoFactorModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/TwoFactorModel.cs new file mode 100644 index 000000000..8055c7677 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Identity/TwoFactorModel.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Web.Models.Identity +{ + public class TwoFactorModel + { + public string Code { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/EditGallery.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/EditGallery.razor deleted file mode 100644 index d61ce2323..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/EditGallery.razor +++ /dev/null @@ -1,197 +0,0 @@ -@page "/edit-gallery" -@using MiniSpace.Web.DTO.Types -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.MediaFiles -@using MiniSpace.Web.Areas.Students -@inject IMediaFilesService MediaFilesService -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject IDialogService DialogService -@using Microsoft.AspNetCore.Components.Forms -@using MudBlazor -@using System.IO -@inject IJSRuntime JS -@using Microsoft.JSInterop - - - - - - - -
- Edit Gallery - @if (isLoading) - { - - } - else - { - - - Gallery Images - @if (studentDto.GalleryOfImageUrls == null || !studentDto.GalleryOfImageUrls.Any(IsValidImageUrl)) - { - No gallery images available - } - else - { - - @foreach (var imageUrl in studentDto.GalleryOfImageUrls.Where(IsValidImageUrl)) - { - - - - - - - - - } - - } -
-
-
-
- } -
-
- -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Account settings", href: "/account", icon: @Icons.Material.Filled.ManageAccounts), - new BreadcrumbItem("Gallery", href: "/edit-gallery", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), - }; - - private bool isLoading = true; - private bool isUploading = false; - private StudentDto studentDto = new(); - private List galleryFiles = new List(); - private long maxFileSize = 32 * 1024 * 1024; - private Dictionary selectedImages = new(); - private readonly List validImageExtensions = new List { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" }; - - protected override async Task OnInitializedAsync() - { - isLoading = true; - try - { - await IdentityService.InitializeAuthenticationState(); - if (IdentityService.IsAuthenticated) - { - var studentId = IdentityService.GetCurrentUserId(); - studentDto = await StudentsService.GetStudentAsync(studentId); - selectedImages = studentDto.GalleryOfImageUrls.ToDictionary(url => url, url => false); - } - else - { - NavigationManager.NavigateTo("/login"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error in OnInitializedAsync: {ex.Message}"); - } - finally - { - isLoading = false; - StateHasChanged(); - } - } - - private void OnGalleryFilesChanged(InputFileChangeEventArgs args) - { - galleryFiles = args.GetMultipleFiles().ToList(); - StateHasChanged(); - } - - private async Task UploadGalleryImagesAsync() - { - if (galleryFiles != null && galleryFiles.Count > 0) - { - isUploading = true; - StateHasChanged(); - try - { - foreach (var file in galleryFiles) - { - using var stream = file.OpenReadStream(maxFileSize); - byte[] bytes = await ReadFully(stream); - var base64Content = Convert.ToBase64String(bytes); - var response = await MediaFilesService.UploadMediaFileAsync(IdentityService.GetCurrentUserId(), - MediaFileContextType.StudentGalleryImage.ToString(), IdentityService.GetCurrentUserId(), - file.Name, file.ContentType, base64Content); - if (response.Content != null && !string.IsNullOrEmpty(response.Content.FileUrl)) - { - studentDto.GalleryOfImageUrls = studentDto.GalleryOfImageUrls.Append(response.Content.FileUrl).ToList(); - selectedImages.Add(response.Content.FileUrl, false); - } - } - } - finally - { - galleryFiles.Clear(); - isUploading = false; - NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); - } - } - } - - private async Task DeleteSelectedImages() - { - var imagesToDelete = selectedImages.Where(kv => kv.Value).Select(kv => kv.Key).ToList(); - foreach (var imageUrl in imagesToDelete) - { - await MediaFilesService.DeleteMediaFileAsync(imageUrl); - studentDto.GalleryOfImageUrls = studentDto.GalleryOfImageUrls.Where(url => url != imageUrl).ToList(); - selectedImages.Remove(imageUrl); - } - NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); - } - - private static async Task ReadFully(Stream input) - { - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; - while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } - } - - private async Task TriggerFileInputClick() - { - var fileInput = await JS.InvokeAsync("document.getElementById", "fileInput"); - await fileInput.InvokeVoidAsync("click"); - } - - private bool IsValidImageUrl(string url) - { - return validImageExtensions.Contains(Path.GetExtension(url)?.ToLower()); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/GalleryComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/GalleryComponent.razor deleted file mode 100644 index 837ce9035..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/GalleryComponent.razor +++ /dev/null @@ -1,217 +0,0 @@ -@page "/student-profile" -@using MiniSpace.Web.DTO.Types -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.MediaFiles -@using MiniSpace.Web.Areas.Students -@using System.IO -@using Microsoft.AspNetCore.Components.Forms -@inject IMediaFilesService MediaFilesService -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject IDialogService DialogService -@using MudBlazor -@inject IJSRuntime JS -@using Microsoft.JSInterop - - -
- Gallery - @if (isLoading) - { - - } - else - { - - - Current Banner - @if (!string.IsNullOrEmpty(studentDto.BannerUrl) && IsValidImageUrl(studentDto.BannerUrl)) - { - - - - Remove Banner Image - } - else - { - No banner image available - } - Change Banner Image - - - - Gallery Images - @if (studentDto.GalleryOfImageUrls == null || !studentDto.GalleryOfImageUrls.Any(IsValidImageUrl)) - { - No gallery images available - } - else - { - - @foreach (var imageUrl in studentDto.GalleryOfImageUrls.Where(IsValidImageUrl)) - { - - - - - - - - } - - } -
-
-
-
- } -
- - - -@code { - private bool isLoading = true; - private bool isUploading = false; // New state for upload progress - private StudentDto studentDto = new(); - private IList galleryFiles = new List(); - - private long maxFileSize = 32 * 1024 * 1024; - private readonly List validImageExtensions = new List { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" }; - - protected override async Task OnInitializedAsync() - { - isLoading = true; - try - { - await IdentityService.InitializeAuthenticationState(); - if (IdentityService.IsAuthenticated) - { - var studentId = IdentityService.GetCurrentUserId(); - studentDto = await StudentsService.GetStudentAsync(studentId); - } - else - { - NavigationManager.NavigateTo("/login"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error in OnInitializedAsync: {ex.Message}"); - } - finally - { - isLoading = false; - StateHasChanged(); - } - } - - private void OnGalleryFilesChanged(InputFileChangeEventArgs args) - { - galleryFiles = args.GetMultipleFiles().ToList(); - StateHasChanged(); - } - - private async Task UploadGalleryImagesAsync() - { - if (galleryFiles != null && galleryFiles.Count > 0) - { - isUploading = true; // Set uploading state to true - StateHasChanged(); - try - { - foreach (var file in galleryFiles) - { - var stream = file.OpenReadStream(maxFileSize); - byte[] bytes = await ReadFully(stream); - var base64Content = Convert.ToBase64String(bytes); - var response = await MediaFilesService.UploadMediaFileAsync(IdentityService.GetCurrentUserId(), - MediaFileContextType.StudentGalleryImage.ToString(), IdentityService.GetCurrentUserId(), - file.Name, file.ContentType, base64Content); - if (response.Content != null && !string.IsNullOrEmpty(response.Content.FileUrl)) - { - studentDto.GalleryOfImageUrls = studentDto.GalleryOfImageUrls.Append(response.Content.FileUrl).ToList(); - } - stream.Close(); - } - } - finally - { - galleryFiles.Clear(); - isUploading = false; // Set uploading state to false - StateHasChanged(); - } - } - } - - private static async Task ReadFully(Stream input) - { - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; - while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } - } - - private void NavigateToBannerUpdate() - { - NavigationManager.NavigateTo("/update-banner"); - } - - private async Task TriggerFileInputClick() - { - var fileInput = await JS.InvokeAsync("document.getElementById", "fileInput"); - await fileInput.InvokeVoidAsync("click"); - } - - private void NavigateToEditGallery() - { - NavigationManager.NavigateTo("/edit-gallery"); - } - - private async Task RemoveBannerImage() - { - try - { - if (!string.IsNullOrEmpty(studentDto.BannerUrl)) - { - Console.WriteLine($"{studentDto.BannerUrl}"); - await MediaFilesService.DeleteMediaFileAsync(studentDto.BannerUrl); - studentDto.BannerUrl = null; - StateHasChanged(); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error removing banner image: {ex.Message}"); - } - } - - private bool IsValidImageUrl(string url) - { - return validImageExtensions.Contains(Path.GetExtension(url)?.ToLower()); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/UpdateBannerImage.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/UpdateBannerImage.razor deleted file mode 100644 index 085fcea67..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/Components/UpdateBannerImage.razor +++ /dev/null @@ -1,190 +0,0 @@ -@page "/update-banner" -@using MiniSpace.Web.DTO.Types -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.MediaFiles -@using Microsoft.AspNetCore.Components.Forms -@inject IMediaFilesService MediaFilesService -@inject IIdentityService IdentityService -@inject IJSRuntime JSRuntime -@using System.IO -@inject NavigationManager NavigationManager -@using MudBlazor - - - - - - - - -
- Update Banner Image - - - @if (isProcessing) - { -
- - - -
- } - -
- - @if (!string.IsNullOrEmpty(croppedImageBase64)) - { -
- Cropped Image -
- } -
-
- - - -@code { - - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Account settings", href: "/account", icon: @Icons.Material.Filled.ManageAccounts), - new BreadcrumbItem("Banner Image", href: "/update-banner", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), - }; - private string croppedImageBase64; - private bool isProcessing = false; - - private DotNetObjectReference dotNetRef; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - dotNetRef = DotNetObjectReference.Create(this); - await JSRuntime.InvokeVoidAsync("GLOBAL.SetDotnetReference", dotNetRef); - } - } - - private async Task HandleImageSelected(InputFileChangeEventArgs e) - { - var imageFile = e.File; - if (imageFile != null) - { - isProcessing = true; - StateHasChanged(); - long maxAllowedSize = 5 * 1024 * 1024; // 5 MB in bytes - - try - { - using var stream = imageFile.OpenReadStream(maxAllowedSize); - using var ms = new MemoryStream(); - await stream.CopyToAsync(ms); - var buffer = ms.ToArray(); - var base64String = Convert.ToBase64String(buffer); - await JSRuntime.InvokeVoidAsync("displayImageAndInitializeCropper", base64String); - } - catch (System.IO.IOException ex) - { - Console.Error.WriteLine($"File size exceeds the maximum limit: {ex.Message}"); - } - finally - { - isProcessing = false; - StateHasChanged(); - } - } - } - - [JSInvokable] - public async Task ReceiveCroppedImage(string base64Result) - { - croppedImageBase64 = base64Result; - StateHasChanged(); - } - - private string GetFileExtensionFromBase64(string base64String) - { - var dataUriPattern = new System.Text.RegularExpressions.Regex(@"^data:(?image\/.+?);base64,(?.+)$"); - var match = dataUriPattern.Match(base64String); - if (match.Success) - { - var type = match.Groups["type"].Value; - return type switch - { - "image/jpeg" => "jpg", - "image/png" => "png", - "image/gif" => "gif", - "image/tiff" => "tiff", - "image/webp" => "webp", - _ => throw new InvalidOperationException("Unsupported image type"), - }; - } - throw new InvalidOperationException("Invalid base64 image data"); - } - - [JSInvokable] - public async Task SaveCroppedImage() - { - try - { - isProcessing = true; - StateHasChanged(); - - var userId = IdentityService.GetCurrentUserId(); - var fileExtension = GetFileExtensionFromBase64(croppedImageBase64); - var randomGuid = Guid.NewGuid().ToString(); - var fileName = $"banner_image_{userId}_{randomGuid}.{fileExtension}"; - var contentType = $"image/{fileExtension}"; - - var base64Content = croppedImageBase64.Split(',')[1]; - var response = await MediaFilesService.UploadMediaFileAsync( - Guid.NewGuid(), - MediaFileContextType.StudentBannerImage.ToString(), - userId, - fileName, - contentType, - base64Content - ); - - if (response != null) - { - NavigationManager.NavigateTo("/account"); - } - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error saving the cropped image: {ex.Message}"); - } - finally - { - isProcessing = false; - StateHasChanged(); - } - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/EmailVerification.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/EmailVerification.razor index 68c2d88ae..a151e4bbf 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/EmailVerification.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/EmailVerification.razor @@ -62,7 +62,7 @@ color: #007BFF; } - +
@code { private SignInModel signInModel = new SignInModel(); + private TwoFactorModel twoFactorModel = new TwoFactorModel(); private bool showError = false; private string errorMessage = string.Empty; private bool rememberMe = false; - private InputType passwordInputType = InputType.Password; - private string passwordInputIcon = Icons.Material.Filled.VisibilityOff; + private MudBlazor.InputType passwordInputType = MudBlazor.InputType.Password; + private string passwordInputIcon = MudBlazor.Icons.Material.Filled.VisibilityOff; + private bool isTwoFactorRequired = false; + private Guid userId; private void OnAlertClose() { @@ -131,15 +162,15 @@ private void TogglePasswordVisibility() { - if (passwordInputType == InputType.Password) + if (passwordInputType == MudBlazor.InputType.Password) { - passwordInputType = InputType.Text; - passwordInputIcon = Icons.Material.Filled.Visibility; + passwordInputType = MudBlazor.InputType.Text; + passwordInputIcon = MudBlazor.Icons.Material.Filled.Visibility; } else { - passwordInputType = InputType.Password; - passwordInputIcon = Icons.Material.Filled.VisibilityOff; + passwordInputType = MudBlazor.InputType.Password; + passwordInputIcon = MudBlazor.Icons.Material.Filled.VisibilityOff; } } @@ -148,6 +179,49 @@ try { var response = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); + + if (response != null && response.Content != null) + { + if (response.Content.IsTwoFactorRequired) + { + isTwoFactorRequired = true; + userId = response.Content.UserId; + twoFactorModel.Code = string.Empty; // Ensure the code is clear + StateHasChanged(); + } + else if (!string.IsNullOrEmpty(response.Content.AccessToken)) + { + await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); + var nextPage = StudentsService.StudentDto.State == "incomplete" ? "/signin/first" : "/"; + NavigationManager.NavigateTo(nextPage, true); + } + else + { + showError = true; + errorMessage = $"Error during sign in: {response?.ErrorMessage?.Reason ?? "Unknown error"}"; + StateHasChanged(); + } + } + else + { + showError = true; + errorMessage = $"Error during sign in: {response?.ErrorMessage?.Reason ?? "Unknown error"}"; + StateHasChanged(); + } + } + catch (Exception ex) + { + showError = true; + errorMessage = $"Error during sign in: {ex.Message}"; + StateHasChanged(); + } + } + + private async Task HandleTwoFactor() + { + try + { + var response = await IdentityService.VerifyTwoFactorCodeAsync(userId, twoFactorModel.Code); if (response != null && response.Content != null && !string.IsNullOrEmpty(response.Content.AccessToken)) { await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); @@ -157,15 +231,15 @@ else { showError = true; - errorMessage = $"Error during sign in: {response?.ErrorMessage?.Reason}"; - StateHasChanged(); + errorMessage = $"Error during 2FA verification: {response?.ErrorMessage?.Reason ?? "Unknown error"}"; + StateHasChanged(); } } catch (Exception ex) { showError = true; - errorMessage = $"Error during sign in: {ex.Message}"; - StateHasChanged(); + errorMessage = $"Error during 2FA verification: {ex.Message}"; + StateHasChanged(); } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor index 7fc1c69b6..1dee402e3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignUp.razor @@ -8,7 +8,7 @@ @inject NavigationManager NavigationManager @inject IErrorMapperService ErrorMapperService - + - - -@code { - private bool pageInitialized = false; - private int value = 1; - int totalPages = 0; - int totalElements = 0; - IEnumerable events; - private SearchEventsModel searchEventsModel = InitializeSearchModel(); - - protected override async Task OnInitializedAsync() - { - var searchEventsCriteria = await LocalStorage.GetItemAsync("searchEventsCriteria"); - if (searchEventsCriteria != null) - { - searchEventsModel = searchEventsCriteria; - } - - await SearchEvents(); - pageInitialized = true; - } - - private static SearchEventsModel InitializeSearchModel() - { - return new() - { - Name = "", - Organizer = "", - Organization = new OrganizationModel(), - Category = "", - State = "", - Friends = new HashSet(), - FriendsEngagementType = "", - DateFrom = DateTime.Now.AddDays(-7), - DateTo = DateTime.Now.AddDays(30), - Pageable = new PageableDto() - { - Page = 1, - Size = 5, - Sort = new SortDto() - { - SortBy = new List() {"startDate", "name"}, - Direction = "asc" - } - } - }; - } - - private async Task SearchEvents() - { - var response = await EventsService.SearchEventsAsync(searchEventsModel.Name, searchEventsModel.Organizer, - searchEventsModel.Organization.Id, searchEventsModel.Organization.RootId, searchEventsModel.Category, - searchEventsModel.State, searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, - searchEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchEventsModel.DateTo.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchEventsModel.Pageable); - if (response.Content != null) - { - totalPages = response.Content.TotalPages; - totalElements = response.Content.TotalElements; - events = response.Content.Content; - } - else - { - totalPages = 0; - totalElements = 0; - events = new List(); - } - - await LocalStorage.SetItemAsync("searchEventsCriteria", searchEventsModel); - } - - private async void SelectedPageChanged(int pageNumber) - { - searchEventsModel.Pageable.Page = pageNumber; - - var tmp = await EventsService.SearchEventsAsync(searchEventsModel.Name, searchEventsModel.Organizer, - searchEventsModel.Organization.Id, searchEventsModel.Organization.RootId, searchEventsModel.Category, - searchEventsModel.State, searchEventsModel.Friends, searchEventsModel.FriendsEngagementType, - searchEventsModel.DateFrom.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), - searchEventsModel.DateTo.ToUniversalTime().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 = "800px", Height = "650px", Resizable = true, Draggable = true, - AutoFocusFirstElement = false - }); - await SearchEvents(); - } - - private async Task OpenFriendsDialog() - { - await DialogService.OpenAsync($"Filter events by friends", - new Dictionary() { { "SearchEventsModel", searchEventsModel } }, - new DialogOptions() { Width = "700px", Height = "512px", Resizable = true, Draggable = true }); - await SearchEvents(); - } - - private Task RedirectToFollowedEvents() - { - NavigationManager.NavigateTo("/events/follow"); - return Task.CompletedTask; - } - - private async Task ClearFilters() - { - searchEventsModel = InitializeSearchModel(); - await SearchEvents(); - } - -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor index 8c3d86d3a..e082e53e4 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor @@ -10,7 +10,7 @@ @using MudBlazor - + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor index bf701de1a..2d96efd9e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor @@ -1,9 +1,10 @@ +@page "/user-information/{UserId:guid}" @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Students @inject IStudentsService StudentsService @inject NavigationManager NavigationManager - @using MudBlazor + @if (student != null) { @@ -14,7 +15,7 @@
- @student.FullName + @($"{student.FirstName} {student.LastName}") @student.Description Edit Profile
@@ -32,16 +33,13 @@ Contact Information - @if (!string.IsNullOrEmpty(student.ContactEmail)) - { - Contact Email: @student.ContactEmail - } + Contact Email: @student.ContactEmail } - @if (!string.IsNullOrEmpty(student.Education)) + @if (student.Education != null && student.Education.Any()) { @@ -50,13 +48,16 @@ Education - @student.Education + @foreach (var education in student.Education) + { + Institution: @education.InstitutionName, Degree: @education.Degree, Period: @education.StartDate.ToString("yyyy-MM-dd") - @education.EndDate.ToString("yyyy-MM-dd") + } } - @if (!string.IsNullOrEmpty(student.WorkPosition) || !string.IsNullOrEmpty(student.Company)) + @if (student.Work != null && student.Work.Any()) { @@ -65,13 +66,9 @@ Work Experience - @if (!string.IsNullOrEmpty(student.WorkPosition)) - { - Position: @student.WorkPosition - } - @if (!string.IsNullOrEmpty(student.Company)) + @foreach (var work in student.Work) { - Company: @student.Company + Position: @work.Position, Company: @work.Company, Period: @work.StartDate.ToString("yyyy-MM-dd") - @work.EndDate.ToString("yyyy-MM-dd") } @@ -89,11 +86,11 @@ @if (student.Languages != null && student.Languages.Any()) { - Languages: @string.Join(", ", student.Languages) + Languages: @string.Join(", ", student.Languages.Select(l => l.ToString())) } @if (student.Interests != null && student.Interests.Any()) { - Interests: @string.Join(", ", student.Interests) + Interests: @string.Join(", ", student.Interests.Select(i => i.ToString())) } @@ -151,7 +148,7 @@ private void NavigateToProfileSettings() { - NavigationManager.NavigateTo("/showaccount"); + NavigationManager.NavigateTo("/account"); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor index bbcb2de70..bc34d256f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -11,7 +11,7 @@
- + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor index 7eb7e04a6..2622bf4df 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor @@ -12,7 +12,7 @@
- + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor index b88b17c2b..d5272016e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor @@ -12,7 +12,7 @@
- + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor index 706b594ee..25c255050 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor @@ -11,7 +11,7 @@ @inject IJSRuntime JSRuntime - + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor index 80a34abc0..c445ebac2 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor @@ -13,7 +13,7 @@
- + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor index a54af09a9..85bbd7b0c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/StudentDetails.razor @@ -15,7 +15,7 @@
- + @@ -48,17 +48,21 @@

Description: @student.Description

- @if (!string.IsNullOrEmpty(student.Education)) + @if (student.Education != null && student.Education.Any()) { -

Education: @student.Education

- } - @if (!string.IsNullOrEmpty(student.WorkPosition)) - { -

Work Position: @student.WorkPosition

+

Education:

+ @foreach (var education in student.Education) + { +

@education.InstitutionName - @education.Degree (@education.StartDate.ToLocalTime().ToString("yyyy-MM-dd") to @education.EndDate.ToLocalTime().ToString("yyyy-MM-dd"))

+ } } - @if (!string.IsNullOrEmpty(student.Company)) + @if (student.Work != null && student.Work.Any()) { -

Company: @student.Company

+

Work:

+ @foreach (var work in student.Work) + { +

@work.Company - @work.Position (@work.StartDate.ToLocalTime().ToString("yyyy-MM-dd") to @work.EndDate.ToLocalTime().ToString("yyyy-MM-dd"))

+ } } @if (student.Languages != null && student.Languages.Any()) { @@ -68,8 +72,7 @@ {

Interests: @string.Join(", ", student.Interests)

} -

Number of Friends: @student.NumberOfFriends

-

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

+

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

State: @student.State

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

@@ -85,14 +88,14 @@ Gallery Images - @if (student.GalleryOfImageUrls != null && student.GalleryOfImageUrls.Any(IsValidImageUrl)) + @if (student.GalleryOfImageUrls != null && student.GalleryOfImageUrls.Any(img => IsValidImageUrl(img.ImageUrl))) { - @foreach (var imageUrl in student.GalleryOfImageUrls.Where(IsValidImageUrl)) + @foreach (var galleryImage in student.GalleryOfImageUrls.Where(img => IsValidImageUrl(img.ImageUrl))) { - + - + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor new file mode 100644 index 000000000..aa6b11471 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor @@ -0,0 +1,127 @@ +@page "/organizations/create" +@inject IOrganizationsService OrganizationsService +@inject ISnackbar Snackbar +@inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@using MiniSpace.Web.Utilities +@using System.IO +@using MiniSpace.Web.Areas.Organizations.CommandsDto +@using MudBlazor + +Create New Organization + + + + + + +
+ + +
+ + @if (organizationModel.Settings != null) + { + Private Organization + Visible to Public + } + + + Create Organization + + + + + Back + +
+
+ +@code { + private MudForm form; + private CreateOrganizationDto organizationModel = new CreateOrganizationDto(); + private bool _isSubmitting = false; + private List _parentOrganizations = new List(); + private string selectedParentId; + + protected override async Task OnInitializedAsync() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var organizations = await OrganizationsService.GetUserOrganizationsAsync(userId); + _parentOrganizations = organizations.ToList(); + selectedParentId = organizationModel.ParentId?.ToString(); + if (organizationModel.Settings == null) + { + organizationModel.Settings = new OrganizationSettingsDto(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load organizations: {ex.Message}", Severity.Error); + } + } + + + private async Task SubmitForm() + { + await form.Validate(); + + if (form.IsValid) + { + _isSubmitting = true; + + try + { + organizationModel.OrganizationId = Guid.NewGuid(); + organizationModel.OwnerId = IdentityService.GetCurrentUserId(); + organizationModel.ParentId = string.IsNullOrEmpty(selectedParentId) ? null : new Guid?(new Guid(selectedParentId)); + + if (organizationModel.ParentId.HasValue) + { + var parentOrg = _parentOrganizations.FirstOrDefault(o => o.Id == organizationModel.ParentId); + organizationModel.RootId = parentOrg?.RootId ?? parentOrg?.Id; + } + else + { + organizationModel.RootId = organizationModel.OrganizationId; + } + + var response = await OrganizationsService.CreateOrganizationAsync(organizationModel); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Organization created successfully.", Severity.Success); + NavigationManager.NavigateTo($"/organizations/details/{organizationModel.OrganizationId}"); + } + else + { + Snackbar.Add("Failed to create organization.", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"An error occurred: {ex.Message}", Severity.Error); + } + + _isSubmitting = false; + } + } + + private void GoBack() + { + NavigationManager.NavigateTo("/organizations/my"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EditOrganization.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EditOrganization.razor new file mode 100644 index 000000000..362e1f75e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EditOrganization.razor @@ -0,0 +1,450 @@ +@page "/organizations/edit/{OrganizationId:guid}" +@inject IOrganizationsService OrganizationsService +@inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService +@inject NavigationManager NavigationManager +@inject IJSRuntime JSRuntime +@inject ISnackbar Snackbar +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.DTO.Enums +@using MudBlazor +@using System.IO +@using MiniSpace.Web.Utilities +@using System.Text.Json + + + + @if (isLoading || IsUploading) + { +
+ + Uploading Image, please wait... +
+ } + else if (organizationGalleryUsers == null) + { + Failed to load organization details. + } + else + { + + +
+ +
+ + +
+ +
+ + + + @organizationGalleryUsers.OrganizationDetails.Name + + + + + + + Save + + +
+ + + + + + Overview + Settings + Members + Roles + Gallery + + + + + @if (selectedTabIndex == 0) + { + Edit Overview + + + + @foreach (var role in organizationGalleryUsers.OrganizationDetails.Roles) + { + @role.Name + } + + } + else if (selectedTabIndex == 1) + { + Organization Settings + @if (organizationGalleryUsers?.OrganizationDetails?.Settings != null) + { + + } + else + { + Settings are not available. + } + } + else if (selectedTabIndex == 2) + { + Manage Members + + } + else if (selectedTabIndex == 3) + { + Manage Roles + + } + else if (selectedTabIndex == 4) + { + Manage Organiztion Galler + + } + + + + + } +
+
+ + + + + + + +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + private OrganizationGalleryUsersDto organizationGalleryUsers; + private bool isLoading = true; + private bool IsUploading { get; set; } = false; + + private int selectedTabIndex = 0; + private string CroppedImageBase64 { get; set; } + + private IBrowserFile croppedImageFile; + private string currentImageType = string.Empty; // "profile" or "banner" + + private string defaultBannerImage = "/images/default_banner_image.png"; + private string defaultProfileImage = "/images/default_organization_profile_image.png"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JSRuntime.InvokeVoidAsync("GLOBAL.SetDotnetReference", DotNetObjectReference.Create(this)); + } + } + + protected override async Task OnInitializedAsync() + { + isLoading = true; + + try + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + Console.WriteLine(JsonSerializer.Serialize(organizationGalleryUsers)); + } + else + { + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + organizationGalleryUsers = null; + } + finally + { + isLoading = false; + } + } + + private string GetBannerUrl() + { + return !string.IsNullOrWhiteSpace(organizationGalleryUsers?.OrganizationDetails?.BannerUrl) + ? organizationGalleryUsers.OrganizationDetails.BannerUrl + : defaultBannerImage; + } + + private string GetProfileImageUrl() + { + return !string.IsNullOrWhiteSpace(organizationGalleryUsers?.OrganizationDetails?.ImageUrl) + ? organizationGalleryUsers.OrganizationDetails.ImageUrl + : defaultProfileImage; + } + + private void LoadTabContent(int index) + { + selectedTabIndex = index; + } + + private async Task OpenCropper(InputFileChangeEventArgs e, string imageType) + { + const long maxAllowedSize = 10 * 1024 * 1024; + var inputFile = e.File; + currentImageType = imageType; + + if (inputFile != null) + { + if (inputFile.Size > maxAllowedSize) + { + await JSRuntime.InvokeVoidAsync("alert", $"File size exceeds the allowed limit of {maxAllowedSize / (1024 * 1024)} MB."); + return; + } + + using var stream = inputFile.OpenReadStream(maxAllowedSize); + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + var buffer = ms.ToArray(); + var base64Image = Convert.ToBase64String(buffer); + await JSRuntime.InvokeVoidAsync("displayImageAndInitializeCropper", base64Image, imageType); + } + } + + + private void CloseCropper() + { + JSRuntime.InvokeVoidAsync("hideCropperModal"); + } + + [JSInvokable] + public void ReceiveCroppedImage(string base64Image) + { + if (!string.IsNullOrEmpty(base64Image)) + { + CroppedImageBase64 = $"data:image/png;base64,{base64Image}"; + var buffer = Convert.FromBase64String(base64Image); + var lastModified = DateTimeOffset.Now; // Use the current time as a placeholder for lastModified + croppedImageFile = new BrowserFile(buffer, "cropped-image.png", "image/png", lastModified); + StateHasChanged(); + } + } + + + + private async Task SaveCroppedImage() + { + if (croppedImageFile != null) + { + IsUploading = true; + StateHasChanged(); // Trigger UI update to show the overlay + + try + { + // Convert IBrowserFile to byte[] + byte[] fileData; + using (var stream = croppedImageFile.OpenReadStream(croppedImageFile.Size)) + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms); + fileData = ms.ToArray(); + } + + string imageType = currentImageType == "profile" ? "OrganizationProfileImage" : "OrganizationBannerImage"; + var response = await MediaFilesService.UploadOrganizationImageAsync( + OrganizationId, + imageType, + IdentityService.GetCurrentUserId(), + $"{organizationGalleryUsers.OrganizationDetails.Name}_{currentImageType}.png", + croppedImageFile.ContentType, + fileData); + + if (response.IsSuccessStatusCode) + { + var uploadedFile = response.Content; + if (uploadedFile != null) + { + if (currentImageType == "profile") + { + organizationGalleryUsers.OrganizationDetails.ImageUrl = uploadedFile.FileUrl; + } + else + { + organizationGalleryUsers.OrganizationDetails.BannerUrl = uploadedFile.FileUrl; + } + + StateHasChanged(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "Failed to upload the image."); + } + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred: {ex.Message}"); + } + finally + { + IsUploading = false; // Hide the loading overlay + StateHasChanged(); + } + + CloseCropper(); + } + } + + + + + private async Task RemoveImage(string imageType) + { + try + { + string imageUrl = imageType == "profile" ? organizationGalleryUsers.OrganizationDetails.ImageUrl : organizationGalleryUsers.OrganizationDetails.BannerUrl; + await MediaFilesService.DeleteMediaFileAsync(imageUrl); + + if (imageType == "profile") + { + organizationGalleryUsers.OrganizationDetails.ImageUrl = null; + } + else + { + organizationGalleryUsers.OrganizationDetails.BannerUrl = null; + } + + StateHasChanged(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred while removing the image: {ex.Message}"); + } + } + + private async Task SaveOrganization() + { + var organizationDetails = organizationGalleryUsers.OrganizationDetails; + + var parentId = organizationDetails.ParentOrganizationId ?? Guid.Empty; + + var updateCommand = new UpdateOrganizationCommand( + OrganizationId, + organizationDetails.Name, + organizationDetails.Description, + Guid.Empty, + parentId, + organizationDetails.OwnerId, + organizationDetails.Settings, + organizationDetails.BannerUrl, + organizationDetails.ImageUrl, + organizationDetails.DefaultRoleName + ); + + var response = await OrganizationsService.UpdateOrganizationAsync(OrganizationId, updateCommand); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Organization updated successfully.", Severity.Success); + NavigationManager.NavigateTo($"/organizations/details/{OrganizationId}"); + } + else + { + Snackbar.Add("Failed to update organization. Please try again.", Severity.Error); + } + } + + private async Task HandleSaveSettings(OrganizationSettingsDto settings) + { + var updateCommand = new UpdateOrganizationSettingsCommand(OrganizationId, settings); + + var response = await OrganizationsService.UpdateOrganizationSettingsAsync(OrganizationId, updateCommand); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Settings updated successfully.", Severity.Success); + } + else + { + Snackbar.Add("Failed to update settings. Please try again.", Severity.Error); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor new file mode 100644 index 000000000..ec793cdef --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor @@ -0,0 +1,90 @@ +@page "/organizations/my" +@inject IOrganizationsService OrganizationService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@inject IIdentityService IdentityService +@using MudBlazor + + + My Organizations + + + + Create Organization + + + + @if (_isLoading) + { + + } + else if (_organizations != null && _organizations.Any()) + { + @foreach (var organization in _organizations) + { + + + + + @organization.Name + @organization.Description + + + + View + + + + + } + } + else + { + No organizations found. + } + + + + +@code { + private bool _isLoading = true; + private List _organizations; + + protected override async Task OnInitializedAsync() + { + _isLoading = true; + + try + { + // Ensure the authentication state is initialized + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + var userId = IdentityService.GetCurrentUserId(); + _organizations = (await OrganizationService.GetRootOrganizationsAsync()).ToList(); + } + else + { + // If the user is not authenticated, navigate to the sign-in page + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load organizations: {ex.Message}", Severity.Error); + } + + _isLoading = false; + } + + private void CreateOrganization() + { + NavigationManager.NavigateTo("/organizations/create"); + } + + private void NavigateToOrganization(Guid organizationId) + { + NavigationManager.NavigateTo($"/organizations/details/{organizationId}"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor new file mode 100644 index 000000000..d07535c1d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor @@ -0,0 +1,197 @@ +@page "/organizations/details/{OrganizationId:guid}" +@inject IOrganizationsService OrganizationsService +@inject IStudentsService StudentsService +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.DTO.Enums +@using MudBlazor + + + + @if (isLoading) + { + + } + else if (organizationGalleryUsers == null) + { + Failed to load organization details. + } + else + { + + + + + +
+ +
+ + + @organizationGalleryUsers.OrganizationDetails.Name + + + + + + @if (isAdmin) + { + + Edit + + } + +
+ + + + + + + Feed + Overview + Posts + Events + Members + Suborganizations + Gallery + + + + + + + @if (selectedTabIndex == 1) + { + + + } + else if (selectedTabIndex == 2) + { + + } + else if (selectedTabIndex == 3) + { + + } + else if (selectedTabIndex == 4) + { + + } + else if (selectedTabIndex == 5) + { + + } + else if (selectedTabIndex == 6) + { + + } + + + + } +
+
+ +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + private OrganizationGalleryUsersDto organizationGalleryUsers; + private bool isAdmin; + private bool isLoading = true; + private int selectedTabIndex = 0; + + private string defaultBannerImage = "/images/default_banner_image.png"; + private string defaultProfileImage = "/images/default_organization_profile_image.png"; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + + try + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + isAdmin = CheckIfUserIsAdminOrHasPermissions(); + } + else + { + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + organizationGalleryUsers = null; + } + finally + { + isLoading = false; + } + } + + private bool CheckIfUserIsAdminOrHasPermissions() + { + var currentUserId = IdentityService.GetCurrentUserId(); + + if (organizationGalleryUsers?.OrganizationDetails?.OwnerId == currentUserId) + { + return true; + } + + var currentUserRole = organizationGalleryUsers?.Users? + .FirstOrDefault(u => u.Id == currentUserId)?.Role; + + if (currentUserRole != null) + { + return currentUserRole.Permissions.TryGetValue(Permission.EditOrganizationDetails, out bool canEdit) && canEdit || + currentUserRole.Permissions.TryGetValue(Permission.UpdateOrganizationImage, out bool canUpdateImage) && canUpdateImage; + } + + return false; + } + + private string GetBannerUrl() + { + return !string.IsNullOrWhiteSpace(organizationGalleryUsers?.OrganizationDetails?.BannerUrl) + ? organizationGalleryUsers.OrganizationDetails.BannerUrl + : defaultBannerImage; + } + + private string GetProfileImageUrl() + { + return !string.IsNullOrWhiteSpace(organizationGalleryUsers?.OrganizationDetails?.ImageUrl) + ? organizationGalleryUsers.OrganizationDetails.ImageUrl + : defaultProfileImage; + } + + private void LoadTabContent(int index) + { + selectedTabIndex = index; + } + + private void NavigateToFeed() + { + NavigationManager.NavigateTo($"/organizations/{OrganizationId}/feed"); + } + private void EditOrganization() + { + NavigationManager.NavigateTo($"/organizations/edit/{OrganizationId}"); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGallery.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGallery.razor new file mode 100644 index 000000000..9f79c5c04 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGallery.razor @@ -0,0 +1,242 @@ +@page "/organizations/{OrganizationId:guid}/gallery" +@inject IOrganizationsService OrganizationsService +@inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService +@inject IJSRuntime JSRuntime +@using MiniSpace.Web.Utilities +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Organizations +@using System.IO +@using MudBlazor + +Organization Gallery +@if (IsLoading) +{ + +} +else +{ + +
+ + Upload New Image + +
+ + @if (GalleryImages == null || !GalleryImages.Any()) + { + No images found in the gallery. + } + else + { + + + @foreach (var image in GalleryImages) + { + + + + } + + } +} + + + + +@if (!string.IsNullOrEmpty(CroppedImageBase64)) +{ +
+ Cropped Image Preview: + +
+} + +@code { + [Parameter] public Guid OrganizationId { get; set; } + private List GalleryImages { get; set; } + private bool IsLoading { get; set; } = true; + private bool IsUploading { get; set; } = false; + private string CroppedImageBase64 { get; set; } + private IBrowserFile croppedImageFile; + private Guid currentImageId; + + protected override async Task OnInitializedAsync() + { + try + { + IsLoading = true; + + // Fetch organization details along with gallery and users + var organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + GalleryImages = organizationGalleryUsers?.Gallery?.ToList() ?? new List(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + GalleryImages = null; + } + finally + { + IsLoading = false; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JSRuntime.InvokeVoidAsync("GLOBAL.SetDotnetReference", DotNetObjectReference.Create(this)); + } + } + + private async Task OpenCropper(InputFileChangeEventArgs e, Guid imageId) + { + const long maxAllowedSize = 10 * 1024 * 1024; + var inputFile = e.File; + currentImageId = imageId; + + if (inputFile != null) + { + if (inputFile.Size > maxAllowedSize) + { + await JSRuntime.InvokeVoidAsync("alert", $"File size exceeds the allowed limit of {maxAllowedSize / (1024 * 1024)} MB."); + return; + } + + using var stream = inputFile.OpenReadStream(maxAllowedSize); + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + var buffer = ms.ToArray(); + var base64Image = Convert.ToBase64String(buffer); + await JSRuntime.InvokeVoidAsync("displayImageAndInitializeCropper", base64Image, "profile"); + } + } + + private void CloseCropper() + { + JSRuntime.InvokeVoidAsync("hideCropperModal"); + } + + [JSInvokable] + public void ReceiveCroppedImage(string base64Image) + { + if (!string.IsNullOrEmpty(base64Image)) + { + CroppedImageBase64 = $"data:image/png;base64,{base64Image}"; + var buffer = Convert.FromBase64String(base64Image); + var lastModified = DateTimeOffset.Now; + croppedImageFile = new BrowserFile(buffer, "cropped-image.png", "image/png", lastModified); + StateHasChanged(); + } + } + + private async Task SaveCroppedImage() + { + if (croppedImageFile != null) + { + IsUploading = true; + StateHasChanged(); + + try + { + byte[] fileData; + using (var stream = croppedImageFile.OpenReadStream(croppedImageFile.Size)) + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms); + fileData = ms.ToArray(); + } + + var response = await MediaFilesService.UploadOrganizationImageAsync( + OrganizationId, + "OrganizationGalleryImage", + IdentityService.GetCurrentUserId(), + $"gallery_image_{currentImageId}.png", + croppedImageFile.ContentType, + fileData); + + // Refresh the gallery images after successful upload + if (response.IsSuccessStatusCode) + { + var organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + GalleryImages = organizationGalleryUsers?.Gallery?.ToList() ?? new List(); + } + + StateHasChanged(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred: {ex.Message}"); + } + + IsUploading = false; + StateHasChanged(); + CloseCropper(); + } + } + + private async Task RemoveImage(Guid imageId) + { + IsUploading = true; + StateHasChanged(); + + try + { + var imageUrl = GalleryImages.FirstOrDefault(img => img.ImageId == imageId)?.ImageUrl; + if (!string.IsNullOrEmpty(imageUrl)) + { + await MediaFilesService.DeleteMediaFileAsync(imageUrl); + GalleryImages.RemoveAll(img => img.ImageId == imageId); + StateHasChanged(); + } + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred while removing the image: {ex.Message}"); + } + + IsUploading = false; + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGalleryComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGalleryComponent.razor new file mode 100644 index 000000000..7b236c9fe --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationGalleryComponent.razor @@ -0,0 +1,276 @@ +@page "/organizations/details/{OrganizationId:guid}/gallery" +@inject IOrganizationsService OrganizationsService +@inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.Utilities +@using MiniSpace.Web.DTO.Enums +@using MudBlazor +@using System.IO + +Organization Gallery +@if (IsLoading) +{ + +} +else +{ + @if (CanManageGallery) + { + +
+ + Upload New Image + +
+ } + + @if (GalleryImages == null || !GalleryImages.Any()) + { + No images found in the gallery. + } + else + { + + + @foreach (var image in GalleryImages) + { + + + + } + + } +} + + + + + +@if (!string.IsNullOrEmpty(CroppedImageBase64)) +{ +
+ Cropped Image Preview: + +
+} + +@code { + [Parameter] public Guid OrganizationId { get; set; } + private List GalleryImages { get; set; } + private bool IsLoading { get; set; } = true; + private bool IsUploading { get; set; } = false; + private string CroppedImageBase64 { get; set; } + private IBrowserFile croppedImageFile; + private Guid currentImageId; + private bool CanManageGallery { get; set; } = false; + + protected override async Task OnInitializedAsync() + { + try + { + IsLoading = true; + + // Fetch organization details along with gallery and users + var organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + GalleryImages = organizationGalleryUsers?.Gallery?.ToList() ?? new List(); + + // Check if the current user has permission to manage the gallery + CanManageGallery = CheckIfUserCanManageGallery(organizationGalleryUsers); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + GalleryImages = null; + } + finally + { + IsLoading = false; + } + } + + private bool CheckIfUserCanManageGallery(OrganizationGalleryUsersDto organizationGalleryUsers) + { + var currentUserId = IdentityService.GetCurrentUserId(); + + // Check if the current user is the owner or has specific permissions + if (organizationGalleryUsers?.OrganizationDetails?.OwnerId == currentUserId) + { + return true; + } + + var currentUserRole = organizationGalleryUsers?.Users? + .FirstOrDefault(u => u.Id == currentUserId)?.Role; + + if (currentUserRole != null) + { + return currentUserRole.Permissions.TryGetValue(Permission.ModifyGallery, out bool canUpload) && canUpload || + currentUserRole.Permissions.TryGetValue(Permission.ModifyGallery, out bool canRemove) && canRemove; + } + + return false; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JSRuntime.InvokeVoidAsync("GLOBAL.SetDotnetReference", DotNetObjectReference.Create(this)); + } + } + + private async Task OpenCropper(InputFileChangeEventArgs e, Guid imageId) + { + const long maxAllowedSize = 10 * 1024 * 1024; + var inputFile = e.File; + currentImageId = imageId; + + if (inputFile != null) + { + if (inputFile.Size > maxAllowedSize) + { + await JSRuntime.InvokeVoidAsync("alert", $"File size exceeds the allowed limit of {maxAllowedSize / (1024 * 1024)} MB."); + return; + } + + using var stream = inputFile.OpenReadStream(maxAllowedSize); + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + var buffer = ms.ToArray(); + var base64Image = Convert.ToBase64String(buffer); + await JSRuntime.InvokeVoidAsync("displayImageAndInitializeCropper", base64Image, "profile"); + } + } + + private void CloseCropper() + { + JSRuntime.InvokeVoidAsync("hideCropperModal"); + } + + [JSInvokable] + public void ReceiveCroppedImage(string base64Image) + { + if (!string.IsNullOrEmpty(base64Image)) + { + CroppedImageBase64 = $"data:image/png;base64,{base64Image}"; + var buffer = Convert.FromBase64String(base64Image); + var lastModified = DateTimeOffset.Now; + croppedImageFile = new BrowserFile(buffer, "cropped-image.png", "image/png", lastModified); + StateHasChanged(); + } + } + + private async Task SaveCroppedImage() + { + if (croppedImageFile != null) + { + IsUploading = true; + StateHasChanged(); + + try + { + byte[] fileData; + using (var stream = croppedImageFile.OpenReadStream(croppedImageFile.Size)) + using (var ms = new MemoryStream()) + { + await stream.CopyToAsync(ms); + fileData = ms.ToArray(); + } + + var response = await MediaFilesService.UploadOrganizationImageAsync( + OrganizationId, + "OrganizationGalleryImage", + IdentityService.GetCurrentUserId(), + $"gallery_image_{currentImageId}.png", + croppedImageFile.ContentType, + fileData); + + // Refresh the gallery images after successful upload + if (response.IsSuccessStatusCode) + { + var organizationGalleryUsers = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + GalleryImages = organizationGalleryUsers?.Gallery?.ToList() ?? new List(); + } + + StateHasChanged(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred: {ex.Message}"); + } + + IsUploading = false; + StateHasChanged(); + CloseCropper(); + } + } + + private async Task RemoveImage(Guid imageId) + { + IsUploading = true; + StateHasChanged(); + + try + { + var imageUrl = GalleryImages.FirstOrDefault(img => img.ImageId == imageId)?.ImageUrl; + if (!string.IsNullOrEmpty(imageUrl)) + { + await MediaFilesService.DeleteMediaFileAsync(imageUrl); + GalleryImages.RemoveAll(img => img.ImageId == imageId); + StateHasChanged(); + } + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"An error occurred while removing the image: {ex.Message}"); + } + + IsUploading = false; + StateHasChanged(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationSettings.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationSettings.razor new file mode 100644 index 000000000..35aade5c9 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationSettings.razor @@ -0,0 +1,71 @@ +@using MiniSpace.Web.DTO.Organizations +@using MudBlazor + +@if (isSaving) +{ +
+ + Saving... +
+} + +@if (Settings != null) +{ + + + + + + + + + + + + + + + + + Save Settings + + +} +else +{ + Settings are not available. +} + +@code { + [Parameter] + public OrganizationSettingsDto Settings { get; set; } + + [Parameter] + public EventCallback OnSave { get; set; } + + private MudForm form; + private bool isSaving = false; + + private async Task SaveSettings() + { + await form.Validate(); + + if (form.IsValid) + { + isSaving = true; + try + { + await OnSave.InvokeAsync(Settings); + } + catch (Exception ex) + { + // Handle exceptions and notify the user + Console.Error.WriteLine(ex.Message); + } + finally + { + isSaving = false; + } + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OverviewComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OverviewComponent.razor new file mode 100644 index 000000000..9674f9e74 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OverviewComponent.razor @@ -0,0 +1,106 @@ +@page "/organizations/overview/{OrganizationId:guid}" +@inject IOrganizationsService OrganizationsService +@inject IIdentityService IdentityService +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.DTO.Enums +@using MudBlazor + + + + @if (isLoading) + { + + } + else if (organization == null) + { + Failed to load organization overview. + } + else + { + + @organization.Name + + + Description + @organization.Description + + Members + + @foreach (var user in organization.Users) + { + + @user.Id - @user.Role.Name + + } + + + + + Roles + + @foreach (var role in organization.Roles) + { + + @role.Name + + } + + + + + Gallery + + @foreach (var image in organization.Gallery) + { + + + + + @image.DateAdded.ToShortDateString() + + + + } + + + } + + + +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + private OrganizationDetailsDto organization; + private bool isLoading = true; + + // Inject NavigationManager here + [Inject] private NavigationManager NavigationManager { get; set; } + + protected override async Task OnInitializedAsync() + { + isLoading = true; + try + { + await IdentityService.InitializeAuthenticationState(); + if (IdentityService.IsAuthenticated) + { + var response = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + organization = response.OrganizationDetails; + } + else + { + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + organization = null; + } + finally + { + isLoading = false; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/RolesComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/RolesComponent.razor new file mode 100644 index 000000000..01ce3bb8a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/RolesComponent.razor @@ -0,0 +1,242 @@ +@page "/organizations/edit/{OrganizationId:guid}/roles" +@inject IOrganizationsService OrganizationsService +@inject IIdentityService IdentityService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.DTO.Enums +@using MudBlazor + +@code { + [Parameter] public Guid OrganizationId { get; set; } + private OrganizationGalleryUsersDto organizationData; + private List organizationRoles = new(); + private RoleDto newRole = new RoleDto { Permissions = new Dictionary() }; + private RoleDto editingRole; + private bool isLoading = true; + private bool isEditing = false; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + + try + { + if (OrganizationId == Guid.Empty) + { + throw new ArgumentException("Invalid OrganizationId"); + } + + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + organizationData = await OrganizationsService.GetOrganizationWithGalleryAndUsersAsync(OrganizationId); + + if (organizationData?.OrganizationDetails != null) + { + organizationRoles = organizationData.OrganizationDetails.Roles.ToList(); + } + + foreach (Permission permission in Enum.GetValues(typeof(Permission))) + { + newRole.Permissions.Add(permission, false); + } + } + else + { + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (ArgumentException ex) + { + Snackbar.Add($"Error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add("Failed to load organization roles.", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private void StartEditing(RoleDto role) + { + isEditing = true; + editingRole = new RoleDto + { + Id = role.Id, + Name = role.Name, + Description = role.Description, + Permissions = new Dictionary(role.Permissions) + }; + } + + private void CancelEditing() + { + isEditing = false; + editingRole = null; + } + + private async Task SaveRole() + { + try + { + if (OrganizationId == Guid.Empty) + { + throw new ArgumentException("Invalid OrganizationId"); + } + + if (isEditing) + { + var command = new UpdateRolePermissionsCommand( + organizationId: OrganizationId, + roleId: editingRole.Id, + roleName: editingRole.Name, + description: editingRole.Description, + permissions: editingRole.Permissions.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value) + ); + + var response = await OrganizationsService.UpdateRolePermissionsAsync(OrganizationId, editingRole.Id, command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Role updated successfully.", Severity.Success); + var roleToUpdate = organizationRoles.FirstOrDefault(r => r.Id == editingRole.Id); + if (roleToUpdate != null) + { + roleToUpdate.Name = editingRole.Name; + roleToUpdate.Description = editingRole.Description; + roleToUpdate.Permissions = new Dictionary(editingRole.Permissions); + } + CancelEditing(); + } + else + { + Snackbar.Add("Failed to update role.", Severity.Error); + } + } + else + { + var command = new CreateOrganizationRoleCommand( + organizationId: OrganizationId, + roleName: newRole.Name, + description: newRole.Description, + permissions: newRole.Permissions.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value) + ); + + var response = await OrganizationsService.CreateOrganizationRoleAsync(OrganizationId, command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Role created successfully.", Severity.Success); + organizationRoles.Add(newRole); + newRole = new RoleDto { Permissions = new Dictionary() }; + foreach (Permission permission in Enum.GetValues(typeof(Permission))) + { + newRole.Permissions.Add(permission, false); + } + } + else + { + Snackbar.Add("Failed to create role.", Severity.Error); + } + } + } + catch (ArgumentException ex) + { + Snackbar.Add($"Error: {ex.Message}", Severity.Error); + } + catch (Exception ex) + { + Snackbar.Add("An error occurred while saving the role.", Severity.Error); + Console.Error.WriteLine($"Exception: {ex}"); + } + } +} + +@if (isLoading) +{ + +} +else if (organizationData?.OrganizationDetails == null) +{ + Failed to load organization roles. +} +else +{ + + Roles + + + Role Name + Description + Permissions + Actions + + + @context.Name + @context.Description + + @foreach (var permission in context.Permissions) + { + @permission.Key.ToString() + } + + + + Edit + + + + + + + + @if (isEditing) + { + Edit Role + + + + Permissions + + @foreach (var permission in editingRole.Permissions.Keys.ToList()) + { + + + + } + + + + Save Role + + + Cancel + + } + else + { + Create New Role + + + + Permissions + + @foreach (var permission in newRole.Permissions.Keys.ToList()) + { + + + + } + + + + Create Role + + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor index 0445e882d..e69de29bb 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor @@ -1,202 +0,0 @@ -@page "/events/{EventId}/posts/create" -@using MiniSpace.Web.Areas.Identity -@using MiniSpace.Web.Areas.Http -@using MiniSpace.Web.Areas.MediaFiles -@using MiniSpace.Web.Areas.Posts -@using MiniSpace.Web.DTO -@using MiniSpace.Web.DTO.Types -@using MiniSpace.Web.Models.Posts -@using Radzen -@using System.IO -@inject IIdentityService IdentityService -@inject IPostsService PostsService -@inject IMediaFilesService MediaFilesService -@inject IErrorMapperService ErrorMapperService -@inject NavigationManager NavigationManager - - -

Create new post

- - - @errorMessage - - - - - - - - - - - - - - - - - - - - - - @if (publishInfo == 2) - { - - - - - } - - - - - - - @if (isUploading) - { - - - - } - - - Choose files to upload (max 3) - - - - - - - - - - - - - - - - -
-@code { - [Parameter] - public string EventId { get; set; } - - private Guid organizerId; - - private CreatePostModel createPostModel = new() - { - TextContent = "Lorem ipsum!", - }; - private bool showError = false; - private string errorMessage = string.Empty; - private int publishInfo = 1; - private TaskCompletionSource clientChangeCompletionSource; - private bool isUploading = false; - private Dictionary images = new (); - - 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.PostId = Guid.NewGuid(); - createPostModel.EventId = new Guid(EventId); - createPostModel.OrganizerId = organizerId; - } - } - - private async Task HandleCreatePost() - { - if (clientChangeCompletionSource != null) - { - await clientChangeCompletionSource.Task; - } - var response = await PostsService.CreatePostAsync(createPostModel.PostId, createPostModel.EventId, - createPostModel.OrganizerId, createPostModel.TextContent, images.Select(i => i.Value), - publishInfo == 2 ? "ToBePublished" : "Published", - publishInfo == 2 ? createPostModel.PublishDate.ToUniversalTime() : null); - - if (response.ErrorMessage != null) - { - showError = true; - errorMessage = ErrorMapperService.MapError(response.ErrorMessage); - } - else - { - NavigationManager.NavigateTo($"/events/{EventId}"); - } - } - - async void OnClientChange(UploadChangeEventArgs args) - { - @* Console.WriteLine("Client-side upload changed"); *@ - clientChangeCompletionSource = new TaskCompletionSource(); - var uploadedImages = new Dictionary(); - isUploading = true; - - foreach (var file in args.Files) - { - StateHasChanged(); - if (images.TryGetValue(file.Name, out var imageId)) - { - uploadedImages.Add(file.Name, imageId); - continue; - } - - try - { - long maxFileSize = 10 * 1024 * 1024; - var stream = file.OpenReadStream(maxFileSize); - byte[] bytes = await ReadFully(stream); - var base64Content = Convert.ToBase64String(bytes); - var response = await MediaFilesService.UploadMediaFileAsync(createPostModel.PostId, - MediaFileContextType.Post.ToString(), IdentityService.UserDto.Id, - file.Name, file.ContentType, base64Content); - if (response.Content != null && response.Content.FileId != Guid.Empty) - { - uploadedImages.Add(file.Name, response.Content.FileId); - } - stream.Close(); - } - catch (Exception ex) - { - @* Console.WriteLine($"Client-side file read error: {ex.Message}"); *@ - } - finally - { - - } - } - isUploading = false; - StateHasChanged(); - images = uploadedImages; - clientChangeCompletionSource.SetResult(true); - } - - private static async Task ReadFully(Stream input) - { - byte[] buffer = new byte[16*1024]; - using MemoryStream ms = new MemoryStream(); - int read; - while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } - return ms.ToArray(); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml index 2b3073e6e..7f0aeb656 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml @@ -13,19 +13,60 @@ + + + + + + + @(await Html.RenderComponentAsync(RenderMode.Server)) - - - - - - @* *@ - - - - - + + + + + + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor deleted file mode 100644 index ca335403f..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/CustomRadzenFooter.razor +++ /dev/null @@ -1,35 +0,0 @@ -@using Radzen -@inherits LayoutComponentBase - - -
-
-
About MiniSpace
-

MiniSpace is a dynamic social platform designed to connect people and communities. Discover, share, and interact with content that drives innovation and engagement.

-
-
-
Quick Links
- -
-
-
Follow Us
-
- - - -
-
-
-
Contact Us
-

Email: info@itsharppro.com

-

Warsaw, Koszykowa 75, 00-062

-
-
-
- © 2024 MiniSpace. All rights reserved. -
-
diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/DisconnectedMessage.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/DisconnectedMessage.razor new file mode 100644 index 000000000..205bf8438 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/DisconnectedMessage.razor @@ -0,0 +1,3 @@ +
+

XXXXXXXXXXXXXXXYou have been disconnected. Please check your internet connection.

+
diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor new file mode 100644 index 000000000..3441d353f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor @@ -0,0 +1,59 @@ +@page "/footer" +@using MudBlazor +@inherits LayoutComponentBase + + + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor index a5f8c3355..f09bf13c7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor @@ -1,193 +1,159 @@ -@using Radzen +@using MudBlazor @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 (_isUserAuthenticated) - { -
- - - - - - - -
- - } - else - { - - - - - - } -
-
-
-
+ + + + + +
+
+ @if (_isUserAuthenticated) + { + + } + else + { + + Menu + + } + MiniSpace +
+ +
+ @if (_isUserAuthenticated) + { + + } +
+
+
- @if (_isUserAuthenticated && _studentState == "valid") + @if (_isUserAuthenticated) { -
- -
+ + +
+ + @_userName +
+
+ + Profile + Settings + Account settings + Notifications + Sign Out + +
} + else + { + + + + + + Home + About App + Connect + Sign In + Sign Up + + + } + @if (_isUserAuthenticated) + { + + + + Menu + + + + Home + Account + + Follow + Search + @if (IdentityService.GetCurrentUserRole() == "organizer") + { + Organize + } + + + Search + Friends + Requests + Sent Requests + + + + Search + My Organizations + Organizations I Follow + -
- @if (_isUserAuthenticated && _studentState == "valid") - { - @if (isPC) - { - - - - - - - - @if (IdentityService.GetCurrentUserRole() == "organizer") - { - - } - - - - - - - - - - - - - - @if (IdentityService.GetCurrentUserRole() == "admin") - { - - - - - - } - - - } - else - { - - - - - - - - - - - @if (IdentityService.GetCurrentUserRole() == "organizer") - { - - } - - - - - - - - - - - - - - @if (IdentityService.GetCurrentUserRole() == "admin") - { - - - - - - } - - - @if (_sidebarExpanded) + + All + New + History + + Reports + @if (IdentityService.GetCurrentUserRole() == "admin") { -
+ + Students + Organizations + Reports + } - } - } + + + } -
-
-
- @*
-
- + +
+
+
+
+
+ @Body
-
*@ -
- @Body
+ @if (_isUserAuthenticated) + { +
+ +
+ }
- @if (_isUserAuthenticated && _studentState == "valid") - { -
- -
- } +
-
+ + - - - - + @code { private bool _sidebarExpanded = false; + private bool _userDrawerOpen = false; private bool isPC = true; - private bool isLoading = true; - private bool firstRender = true; private bool _isUserAuthenticated; private string _userName; private string _userEmail; private Guid _studentId; private string _studentState; - private string currentRoute; + private string _userAvatar; [Inject] IJSRuntime JSRuntime { get; set; } @@ -208,6 +174,7 @@ if (studentDto != null) { _studentState = studentDto.State; + _userAvatar = studentDto.ProfileImageUrl; } } } @@ -220,7 +187,7 @@ public async Task UpdateScreenSize() { var screenSize = await JSRuntime.InvokeAsync("getScreenSize"); - isPC = screenSize.Width > 868; // Adjust this width based on your breakpoint preference + isPC = screenSize.Width > 868; StateHasChanged(); } @@ -238,13 +205,44 @@ public bool IsUserAuthenticated => _isUserAuthenticated; + private void ToggleDrawer() + { + _sidebarExpanded = !_sidebarExpanded; + } + + private void OpenDrawer() + { + _sidebarExpanded = true; + } + + private void ToggleUserDrawer() + { + _userDrawerOpen = !_userDrawerOpen; + } + async Task SignOut() { await localStorage.ClearAsync(); + await IdentityService.Logout(); StateHasChanged(); NavigationManager.NavigateTo("signin", forceLoad: true); } + private async Task ScrollToHome(MouseEventArgs e) + { + await ScrollToSection("home"); + } + + private async Task ScrollToAbout(MouseEventArgs e) + { + await ScrollToSection("about"); + } + + private async Task ScrollToConnect(MouseEventArgs e) + { + await ScrollToSection("connect"); + } + async Task ScrollToSection(string sectionId) { if (NavigationManager.Uri != NavigationManager.BaseUri) @@ -253,7 +251,7 @@ } while (NavigationManager.Uri != NavigationManager.BaseUri) { - await Task.Delay(100); // Check every 100ms + await Task.Delay(100); } await JSRuntime.InvokeVoidAsync("scrollToSection", sectionId); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/ReconnectMessage.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/ReconnectMessage.razor new file mode 100644 index 000000000..d2a07526e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/ReconnectMessage.razor @@ -0,0 +1,8 @@ +
+

XXXXXXXXXXXXXXXXXXXAttempting to reconnect to the server: @CurrentAttempt of @MaxAttempts...

+
+ +@code { + [Parameter] public int CurrentAttempt { get; set; } + [Parameter] public int MaxAttempts { get; set; } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Utilities/BrowserFile.cs b/MiniSpace.Web/src/MiniSpace.Web/Utilities/BrowserFile.cs new file mode 100644 index 000000000..a0a906c88 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Utilities/BrowserFile.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Components.Forms; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Utilities +{ + public class BrowserFile : IBrowserFile + { + private readonly byte[] _buffer; + + public BrowserFile(byte[] buffer, string name, string contentType, DateTimeOffset lastModified) + { + _buffer = buffer; + Name = name; + ContentType = contentType; + LastModified = lastModified; + } + + public string Name { get; } + public string ContentType { get; } + public long Size => _buffer.Length; + + // New property for LastModified + public DateTimeOffset LastModified { get; } + + public Stream OpenReadStream(long maxAllowedSize = 10 * 1024 * 1024, CancellationToken cancellationToken = default) + { + return new MemoryStream(_buffer); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor index 01a332d01..3668439b2 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor @@ -9,5 +9,13 @@ @using MiniSpace.Web.Shared @using MiniSpace.Web.Models.Identity @using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Areas.MediaFiles +@using MiniSpace.Web.Areas.Organizations @using Radzen.Blazor @using Cropper.Blazor.Components +@using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Interests +@using MiniSpace.Web.DTO.Languages +@using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.Areas.Organizations.CommandsDto diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css index 18777ada0..df292aac3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css @@ -58,9 +58,6 @@ app { background-color: rgba(0,0,0,0.4); } -.sidebar .navbar-brand { - font-size: 1.1rem; -} .sidebar .oi { width: 2rem; @@ -69,41 +66,7 @@ app { top: -2px; } -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - -.nav-item:first-of-type { - padding-top: 1rem; -} - -.nav-item:last-of-type { - padding-bottom: 1rem; -} - -.nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; -} - -.nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; -} - -.nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} -.content { - padding-top: 1.1rem; -} .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); @@ -167,13 +130,12 @@ app { height: 100vh; } -header { +/* header { background-color: var(--header-color);; color: white; padding: 0.5rem 1rem; display: flex; flex-wrap: wrap; - align-items: center; justify-content: space-between; } @@ -183,7 +145,7 @@ header { display: flex; align-items: center; width: 100%; -} +} */ .menu-items { display: flex; @@ -216,20 +178,6 @@ main { margin-bottom: 20px; } -.landing-page { - /* padding: 2rem; */ -} - -@media (min-width: 768px) { - .landing-page { - /* padding: 2rem 5rem; */ - } -} -@media (min-width: 992px) { - .landing-page { - /* padding: 2rem 10rem; */ - } -} html, body { height: 100%; @@ -413,7 +361,7 @@ html, body { .left-side, .right-side { width: 100%; - flex: none !important; + /* flex: none !important; */ } .left-side { @@ -639,15 +587,7 @@ html, body { } } -@media (max-width: 600px) { - .small-button { - padding: 0.15rem 0.3rem !important; - font-size: 0.65rem !important; - } - .button-text { - display: none !important; - } -} + .center-container { @@ -657,3 +597,289 @@ html, body { max-width: 900px !important; margin: auto !important; } + +.footer { + background-color: #343a40; + color: #ffffff; + z-index: 1000; + position: relative; +} + +.footer a { + color: #ffffff; + text-decoration: none; +} + +.footer a:hover { + color: #dddddd; +} + +/* site.css */ +.menu-container { + display: flex; + align-items: center; +} + +.mud-menu-item { + display: flex; + align-items: center; +} + +.mud-menu-item .mud-icon { + margin-right: 8px; +} + +.mud-avatar { + margin-right: 16px; +} + + + + + +#components-reconnect-modal { + display: none; + position: fixed; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + z-index: 1000; + overflow: hidden; + background-color: rgb(255, 255, 255); + backdrop-filter: blur(10px); + opacity: 0.8; + text-align: center; + font-weight: bold; + align-items: center; + justify-content: center; + flex-direction: column; +} +#components-reconnect-modal.components-reconnect-show { + display: flex; +} +#components-reconnect-modal.components-reconnect-show div.reconnecting { + display: block; +} +div.reconnecting { + display: none; +} + +#components-reconnect-modal.components-reconnect-failed { + display: block; +} +#components-reconnect-modal.components-reconnect-failed div.failedToConnect { + display: block; +} +div.failedToConnect { + display: none; +} + +#components-reconnect-modal.components-reconnect-rejected { + display: block; +} +#components-reconnect-modal.components-reconnect-rejected div.connectionRejected { + display: block; +} +div.connectionRejected { + display: none; +} + +#components-reconnect-modal.components-reconnect-hide { + display: none; +} +.blur-background { + filter: blur(10px); +} + +.loading { + font-size: 1.5rem; + text-align: center; + margin-top: 20px; +} + +.loading span { + display: inline-block; + animation: loading 1.4s linear infinite; +} + +.loading span:nth-child(1) { + animation-delay: 0s; +} + +.loading span:nth-child(2) { + animation-delay: 0.2s; +} + +.loading span:nth-child(3) { + animation-delay: 0.4s; +} + +.loading span:nth-child(4) { + animation-delay: 0.6s; +} + +.loading span:nth-child(5) { + animation-delay: 0.8s; +} + +.loading span:nth-child(6) { + animation-delay: 1s; +} + +@keyframes loading { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.5); + opacity: 0.5; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + + +.loading-animation { + display: flex; + justify-content: center; + align-items: center; + height: 100px; +} + +.circle { + width: 15px; + height: 15px; + margin: 5px; + background-color: #3498db; /* You can change the color to fit your theme */ + border-radius: 50%; + animation: bounce 1.5s infinite ease-in-out; +} + +.circle:nth-child(2) { + animation-delay: 0.1s; +} + +.circle:nth-child(3) { + animation-delay: 0.2s; +} + +.circle:nth-child(4) { + animation-delay: 0.3s; +} + +.circle:nth-child(5) { + animation-delay: 0.4s; +} + +.circle:nth-child(6) { + animation-delay: 0.5s; +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-15px); + } +} + +.overlay-content { + text-align: center; + padding: 20px; +} + +.overlay-button { + margin-top: 20px; + padding: 10px 20px; + background-color: #3498db; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.overlay-button:hover { + background-color: #2980b9; +} + + + +@media (min-width: 600px) { + .profile-grid { + flex-direction: row; + } +} + +.small-button { + padding: 0.15rem 0.8rem !important; + font-size: 0.65rem !important; + min-width: auto; + display: inline-block; +} + +@media (max-width: 600px) { + .small-button { + + } +} + +.button-margins { + margin-right: 8px; + margin-bottom: 8px; +} + +.upload-button-container { + text-align: center; + margin-bottom: 20px; +} + +.upload-button { + width: 100%; + max-width: 300px; +} + +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; +} + +.gallery-item { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.gallery-image { + width: 100%; + height: auto; + max-height: 200px; + object-fit: cover; + border-radius: 8px; +} + +.delete-button { + margin-top: 8px; + width: 100%; +} + +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 100000; + backdrop-filter: blur(10px); +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/icons/menu-left.svg b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/icons/menu-left.svg new file mode 100644 index 000000000..5d14b1c57 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/icons/menu-left.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_banner_image.png b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_banner_image.png new file mode 100644 index 000000000..1c8c542f3 Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_banner_image.png differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_organization_profile_image.png b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_organization_profile_image.png new file mode 100644 index 000000000..af0e1a4bb Binary files /dev/null and b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/default_organization_profile_image.png differ diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/failed_to_connect.svg b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/failed_to_connect.svg new file mode 100644 index 000000000..1d44b468f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/images/failed_to_connect.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.js b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.js new file mode 100644 index 000000000..6d9549d99 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.js @@ -0,0 +1,3894 @@ +/*! + * Bootstrap v4.0.0 (https://getbootstrap.com) + * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery'), require('popper.js')) : + typeof define === 'function' && define.amd ? define(['exports', 'jquery', 'popper.js'], factory) : + (factory((global.bootstrap = {}),global.jQuery,global.Popper)); +}(this, (function (exports,$,Popper) { 'use strict'; + +$ = $ && $.hasOwnProperty('default') ? $['default'] : $; +Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper; + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _extends() { + _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + return _extends.apply(this, arguments); +} + +function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): util.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Util = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Private TransitionEnd Helpers + * ------------------------------------------------------------------------ + */ + var transition = false; + var MAX_UID = 1000000; // Shoutout AngusCroll (https://goo.gl/pxwQGp) + + function toType(obj) { + return {}.toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); + } + + function getSpecialTransitionEndEvent() { + return { + bindType: transition.end, + delegateType: transition.end, + handle: function handle(event) { + if ($$$1(event.target).is(this)) { + return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params + } + + return undefined; // eslint-disable-line no-undefined + } + }; + } + + function transitionEndTest() { + if (typeof window !== 'undefined' && window.QUnit) { + return false; + } + + return { + end: 'transitionend' + }; + } + + function transitionEndEmulator(duration) { + var _this = this; + + var called = false; + $$$1(this).one(Util.TRANSITION_END, function () { + called = true; + }); + setTimeout(function () { + if (!called) { + Util.triggerTransitionEnd(_this); + } + }, duration); + return this; + } + + function setTransitionEndSupport() { + transition = transitionEndTest(); + $$$1.fn.emulateTransitionEnd = transitionEndEmulator; + + if (Util.supportsTransitionEnd()) { + $$$1.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent(); + } + } + + function escapeId(selector) { + // We escape IDs in case of special selectors (selector = '#myId:something') + // $.escapeSelector does not exist in jQuery < 3 + selector = typeof $$$1.escapeSelector === 'function' ? $$$1.escapeSelector(selector).substr(1) : selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); + return selector; + } + /** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ + + + var Util = { + TRANSITION_END: 'bsTransitionEnd', + getUID: function getUID(prefix) { + do { + // eslint-disable-next-line no-bitwise + prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here + } while (document.getElementById(prefix)); + + return prefix; + }, + getSelectorFromElement: function getSelectorFromElement(element) { + var selector = element.getAttribute('data-target'); + + if (!selector || selector === '#') { + selector = element.getAttribute('href') || ''; + } // If it's an ID + + + if (selector.charAt(0) === '#') { + selector = escapeId(selector); + } + + try { + var $selector = $$$1(document).find(selector); + return $selector.length > 0 ? selector : null; + } catch (err) { + return null; + } + }, + reflow: function reflow(element) { + return element.offsetHeight; + }, + triggerTransitionEnd: function triggerTransitionEnd(element) { + $$$1(element).trigger(transition.end); + }, + supportsTransitionEnd: function supportsTransitionEnd() { + return Boolean(transition); + }, + isElement: function isElement(obj) { + return (obj[0] || obj).nodeType; + }, + typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) { + for (var property in configTypes) { + if (Object.prototype.hasOwnProperty.call(configTypes, property)) { + var expectedTypes = configTypes[property]; + var value = config[property]; + var valueType = value && Util.isElement(value) ? 'element' : toType(value); + + if (!new RegExp(expectedTypes).test(valueType)) { + throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\".")); + } + } + } + } + }; + setTransitionEndSupport(); + return Util; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Alert = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'alert'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.alert'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var TRANSITION_DURATION = 150; + var Selector = { + DISMISS: '[data-dismiss="alert"]' + }; + var Event = { + CLOSE: "close" + EVENT_KEY, + CLOSED: "closed" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + ALERT: 'alert', + FADE: 'fade', + SHOW: 'show' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Alert = + /*#__PURE__*/ + function () { + function Alert(element) { + this._element = element; + } // Getters + + + var _proto = Alert.prototype; + + // Public + _proto.close = function close(element) { + element = element || this._element; + + var rootElement = this._getRootElement(element); + + var customEvent = this._triggerCloseEvent(rootElement); + + if (customEvent.isDefaultPrevented()) { + return; + } + + this._removeElement(rootElement); + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + this._element = null; + }; // Private + + + _proto._getRootElement = function _getRootElement(element) { + var selector = Util.getSelectorFromElement(element); + var parent = false; + + if (selector) { + parent = $$$1(selector)[0]; + } + + if (!parent) { + parent = $$$1(element).closest("." + ClassName.ALERT)[0]; + } + + return parent; + }; + + _proto._triggerCloseEvent = function _triggerCloseEvent(element) { + var closeEvent = $$$1.Event(Event.CLOSE); + $$$1(element).trigger(closeEvent); + return closeEvent; + }; + + _proto._removeElement = function _removeElement(element) { + var _this = this; + + $$$1(element).removeClass(ClassName.SHOW); + + if (!Util.supportsTransitionEnd() || !$$$1(element).hasClass(ClassName.FADE)) { + this._destroyElement(element); + + return; + } + + $$$1(element).one(Util.TRANSITION_END, function (event) { + return _this._destroyElement(element, event); + }).emulateTransitionEnd(TRANSITION_DURATION); + }; + + _proto._destroyElement = function _destroyElement(element) { + $$$1(element).detach().trigger(Event.CLOSED).remove(); + }; // Static + + + Alert._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $element = $$$1(this); + var data = $element.data(DATA_KEY); + + if (!data) { + data = new Alert(this); + $element.data(DATA_KEY, data); + } + + if (config === 'close') { + data[config](this); + } + }); + }; + + Alert._handleDismiss = function _handleDismiss(alertInstance) { + return function (event) { + if (event) { + event.preventDefault(); + } + + alertInstance.close(this); + }; + }; + + _createClass(Alert, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }]); + return Alert; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Alert._jQueryInterface; + $$$1.fn[NAME].Constructor = Alert; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Alert._jQueryInterface; + }; + + return Alert; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Button = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'button'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.button'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var ClassName = { + ACTIVE: 'active', + BUTTON: 'btn', + FOCUS: 'focus' + }; + var Selector = { + DATA_TOGGLE_CARROT: '[data-toggle^="button"]', + DATA_TOGGLE: '[data-toggle="buttons"]', + INPUT: 'input', + ACTIVE: '.active', + BUTTON: '.btn' + }; + var Event = { + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY, + FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY + DATA_API_KEY + " " + ("blur" + EVENT_KEY + DATA_API_KEY) + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Button = + /*#__PURE__*/ + function () { + function Button(element) { + this._element = element; + } // Getters + + + var _proto = Button.prototype; + + // Public + _proto.toggle = function toggle() { + var triggerChangeEvent = true; + var addAriaPressed = true; + var rootElement = $$$1(this._element).closest(Selector.DATA_TOGGLE)[0]; + + if (rootElement) { + var input = $$$1(this._element).find(Selector.INPUT)[0]; + + if (input) { + if (input.type === 'radio') { + if (input.checked && $$$1(this._element).hasClass(ClassName.ACTIVE)) { + triggerChangeEvent = false; + } else { + var activeElement = $$$1(rootElement).find(Selector.ACTIVE)[0]; + + if (activeElement) { + $$$1(activeElement).removeClass(ClassName.ACTIVE); + } + } + } + + if (triggerChangeEvent) { + if (input.hasAttribute('disabled') || rootElement.hasAttribute('disabled') || input.classList.contains('disabled') || rootElement.classList.contains('disabled')) { + return; + } + + input.checked = !$$$1(this._element).hasClass(ClassName.ACTIVE); + $$$1(input).trigger('change'); + } + + input.focus(); + addAriaPressed = false; + } + } + + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', !$$$1(this._element).hasClass(ClassName.ACTIVE)); + } + + if (triggerChangeEvent) { + $$$1(this._element).toggleClass(ClassName.ACTIVE); + } + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + this._element = null; + }; // Static + + + Button._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + if (!data) { + data = new Button(this); + $$$1(this).data(DATA_KEY, data); + } + + if (config === 'toggle') { + data[config](); + } + }); + }; + + _createClass(Button, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }]); + return Button; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, function (event) { + event.preventDefault(); + var button = event.target; + + if (!$$$1(button).hasClass(ClassName.BUTTON)) { + button = $$$1(button).closest(Selector.BUTTON); + } + + Button._jQueryInterface.call($$$1(button), 'toggle'); + }).on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, function (event) { + var button = $$$1(event.target).closest(Selector.BUTTON)[0]; + $$$1(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type)); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Button._jQueryInterface; + $$$1.fn[NAME].Constructor = Button; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Button._jQueryInterface; + }; + + return Button; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Carousel = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'carousel'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.carousel'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var TRANSITION_DURATION = 600; + var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key + + var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key + + var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch + + var Default = { + interval: 5000, + keyboard: true, + slide: false, + pause: 'hover', + wrap: true + }; + var DefaultType = { + interval: '(number|boolean)', + keyboard: 'boolean', + slide: '(boolean|string)', + pause: '(string|boolean)', + wrap: 'boolean' + }; + var Direction = { + NEXT: 'next', + PREV: 'prev', + LEFT: 'left', + RIGHT: 'right' + }; + var Event = { + SLIDE: "slide" + EVENT_KEY, + SLID: "slid" + EVENT_KEY, + KEYDOWN: "keydown" + EVENT_KEY, + MOUSEENTER: "mouseenter" + EVENT_KEY, + MOUSELEAVE: "mouseleave" + EVENT_KEY, + TOUCHEND: "touchend" + EVENT_KEY, + LOAD_DATA_API: "load" + EVENT_KEY + DATA_API_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + CAROUSEL: 'carousel', + ACTIVE: 'active', + SLIDE: 'slide', + RIGHT: 'carousel-item-right', + LEFT: 'carousel-item-left', + NEXT: 'carousel-item-next', + PREV: 'carousel-item-prev', + ITEM: 'carousel-item' + }; + var Selector = { + ACTIVE: '.active', + ACTIVE_ITEM: '.active.carousel-item', + ITEM: '.carousel-item', + NEXT_PREV: '.carousel-item-next, .carousel-item-prev', + INDICATORS: '.carousel-indicators', + DATA_SLIDE: '[data-slide], [data-slide-to]', + DATA_RIDE: '[data-ride="carousel"]' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Carousel = + /*#__PURE__*/ + function () { + function Carousel(element, config) { + this._items = null; + this._interval = null; + this._activeElement = null; + this._isPaused = false; + this._isSliding = false; + this.touchTimeout = null; + this._config = this._getConfig(config); + this._element = $$$1(element)[0]; + this._indicatorsElement = $$$1(this._element).find(Selector.INDICATORS)[0]; + + this._addEventListeners(); + } // Getters + + + var _proto = Carousel.prototype; + + // Public + _proto.next = function next() { + if (!this._isSliding) { + this._slide(Direction.NEXT); + } + }; + + _proto.nextWhenVisible = function nextWhenVisible() { + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && $$$1(this._element).is(':visible') && $$$1(this._element).css('visibility') !== 'hidden') { + this.next(); + } + }; + + _proto.prev = function prev() { + if (!this._isSliding) { + this._slide(Direction.PREV); + } + }; + + _proto.pause = function pause(event) { + if (!event) { + this._isPaused = true; + } + + if ($$$1(this._element).find(Selector.NEXT_PREV)[0] && Util.supportsTransitionEnd()) { + Util.triggerTransitionEnd(this._element); + this.cycle(true); + } + + clearInterval(this._interval); + this._interval = null; + }; + + _proto.cycle = function cycle(event) { + if (!event) { + this._isPaused = false; + } + + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + + if (this._config.interval && !this._isPaused) { + this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval); + } + }; + + _proto.to = function to(index) { + var _this = this; + + this._activeElement = $$$1(this._element).find(Selector.ACTIVE_ITEM)[0]; + + var activeIndex = this._getItemIndex(this._activeElement); + + if (index > this._items.length - 1 || index < 0) { + return; + } + + if (this._isSliding) { + $$$1(this._element).one(Event.SLID, function () { + return _this.to(index); + }); + return; + } + + if (activeIndex === index) { + this.pause(); + this.cycle(); + return; + } + + var direction = index > activeIndex ? Direction.NEXT : Direction.PREV; + + this._slide(direction, this._items[index]); + }; + + _proto.dispose = function dispose() { + $$$1(this._element).off(EVENT_KEY); + $$$1.removeData(this._element, DATA_KEY); + this._items = null; + this._config = null; + this._element = null; + this._interval = null; + this._isPaused = null; + this._isSliding = null; + this._activeElement = null; + this._indicatorsElement = null; + }; // Private + + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, Default, config); + Util.typeCheckConfig(NAME, config, DefaultType); + return config; + }; + + _proto._addEventListeners = function _addEventListeners() { + var _this2 = this; + + if (this._config.keyboard) { + $$$1(this._element).on(Event.KEYDOWN, function (event) { + return _this2._keydown(event); + }); + } + + if (this._config.pause === 'hover') { + $$$1(this._element).on(Event.MOUSEENTER, function (event) { + return _this2.pause(event); + }).on(Event.MOUSELEAVE, function (event) { + return _this2.cycle(event); + }); + + if ('ontouchstart' in document.documentElement) { + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + $$$1(this._element).on(Event.TOUCHEND, function () { + _this2.pause(); + + if (_this2.touchTimeout) { + clearTimeout(_this2.touchTimeout); + } + + _this2.touchTimeout = setTimeout(function (event) { + return _this2.cycle(event); + }, TOUCHEVENT_COMPAT_WAIT + _this2._config.interval); + }); + } + } + }; + + _proto._keydown = function _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + + switch (event.which) { + case ARROW_LEFT_KEYCODE: + event.preventDefault(); + this.prev(); + break; + + case ARROW_RIGHT_KEYCODE: + event.preventDefault(); + this.next(); + break; + + default: + } + }; + + _proto._getItemIndex = function _getItemIndex(element) { + this._items = $$$1.makeArray($$$1(element).parent().find(Selector.ITEM)); + return this._items.indexOf(element); + }; + + _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) { + var isNextDirection = direction === Direction.NEXT; + var isPrevDirection = direction === Direction.PREV; + + var activeIndex = this._getItemIndex(activeElement); + + var lastItemIndex = this._items.length - 1; + var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex; + + if (isGoingToWrap && !this._config.wrap) { + return activeElement; + } + + var delta = direction === Direction.PREV ? -1 : 1; + var itemIndex = (activeIndex + delta) % this._items.length; + return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex]; + }; + + _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) { + var targetIndex = this._getItemIndex(relatedTarget); + + var fromIndex = this._getItemIndex($$$1(this._element).find(Selector.ACTIVE_ITEM)[0]); + + var slideEvent = $$$1.Event(Event.SLIDE, { + relatedTarget: relatedTarget, + direction: eventDirectionName, + from: fromIndex, + to: targetIndex + }); + $$$1(this._element).trigger(slideEvent); + return slideEvent; + }; + + _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) { + if (this._indicatorsElement) { + $$$1(this._indicatorsElement).find(Selector.ACTIVE).removeClass(ClassName.ACTIVE); + + var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)]; + + if (nextIndicator) { + $$$1(nextIndicator).addClass(ClassName.ACTIVE); + } + } + }; + + _proto._slide = function _slide(direction, element) { + var _this3 = this; + + var activeElement = $$$1(this._element).find(Selector.ACTIVE_ITEM)[0]; + + var activeElementIndex = this._getItemIndex(activeElement); + + var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement); + + var nextElementIndex = this._getItemIndex(nextElement); + + var isCycling = Boolean(this._interval); + var directionalClassName; + var orderClassName; + var eventDirectionName; + + if (direction === Direction.NEXT) { + directionalClassName = ClassName.LEFT; + orderClassName = ClassName.NEXT; + eventDirectionName = Direction.LEFT; + } else { + directionalClassName = ClassName.RIGHT; + orderClassName = ClassName.PREV; + eventDirectionName = Direction.RIGHT; + } + + if (nextElement && $$$1(nextElement).hasClass(ClassName.ACTIVE)) { + this._isSliding = false; + return; + } + + var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName); + + if (slideEvent.isDefaultPrevented()) { + return; + } + + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + return; + } + + this._isSliding = true; + + if (isCycling) { + this.pause(); + } + + this._setActiveIndicatorElement(nextElement); + + var slidEvent = $$$1.Event(Event.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }); + + if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.SLIDE)) { + $$$1(nextElement).addClass(orderClassName); + Util.reflow(nextElement); + $$$1(activeElement).addClass(directionalClassName); + $$$1(nextElement).addClass(directionalClassName); + $$$1(activeElement).one(Util.TRANSITION_END, function () { + $$$1(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName.ACTIVE); + $$$1(activeElement).removeClass(ClassName.ACTIVE + " " + orderClassName + " " + directionalClassName); + _this3._isSliding = false; + setTimeout(function () { + return $$$1(_this3._element).trigger(slidEvent); + }, 0); + }).emulateTransitionEnd(TRANSITION_DURATION); + } else { + $$$1(activeElement).removeClass(ClassName.ACTIVE); + $$$1(nextElement).addClass(ClassName.ACTIVE); + this._isSliding = false; + $$$1(this._element).trigger(slidEvent); + } + + if (isCycling) { + this.cycle(); + } + }; // Static + + + Carousel._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + var _config = _extends({}, Default, $$$1(this).data()); + + if (typeof config === 'object') { + _config = _extends({}, _config, config); + } + + var action = typeof config === 'string' ? config : _config.slide; + + if (!data) { + data = new Carousel(this, _config); + $$$1(this).data(DATA_KEY, data); + } + + if (typeof config === 'number') { + data.to(config); + } else if (typeof action === 'string') { + if (typeof data[action] === 'undefined') { + throw new TypeError("No method named \"" + action + "\""); + } + + data[action](); + } else if (_config.interval) { + data.pause(); + data.cycle(); + } + }); + }; + + Carousel._dataApiClickHandler = function _dataApiClickHandler(event) { + var selector = Util.getSelectorFromElement(this); + + if (!selector) { + return; + } + + var target = $$$1(selector)[0]; + + if (!target || !$$$1(target).hasClass(ClassName.CAROUSEL)) { + return; + } + + var config = _extends({}, $$$1(target).data(), $$$1(this).data()); + var slideIndex = this.getAttribute('data-slide-to'); + + if (slideIndex) { + config.interval = false; + } + + Carousel._jQueryInterface.call($$$1(target), config); + + if (slideIndex) { + $$$1(target).data(DATA_KEY).to(slideIndex); + } + + event.preventDefault(); + }; + + _createClass(Carousel, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }]); + return Carousel; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler); + $$$1(window).on(Event.LOAD_DATA_API, function () { + $$$1(Selector.DATA_RIDE).each(function () { + var $carousel = $$$1(this); + + Carousel._jQueryInterface.call($carousel, $carousel.data()); + }); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Carousel._jQueryInterface; + $$$1.fn[NAME].Constructor = Carousel; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Carousel._jQueryInterface; + }; + + return Carousel; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Collapse = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'collapse'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.collapse'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var TRANSITION_DURATION = 600; + var Default = { + toggle: true, + parent: '' + }; + var DefaultType = { + toggle: 'boolean', + parent: '(string|element)' + }; + var Event = { + SHOW: "show" + EVENT_KEY, + SHOWN: "shown" + EVENT_KEY, + HIDE: "hide" + EVENT_KEY, + HIDDEN: "hidden" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + SHOW: 'show', + COLLAPSE: 'collapse', + COLLAPSING: 'collapsing', + COLLAPSED: 'collapsed' + }; + var Dimension = { + WIDTH: 'width', + HEIGHT: 'height' + }; + var Selector = { + ACTIVES: '.show, .collapsing', + DATA_TOGGLE: '[data-toggle="collapse"]' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Collapse = + /*#__PURE__*/ + function () { + function Collapse(element, config) { + this._isTransitioning = false; + this._element = element; + this._config = this._getConfig(config); + this._triggerArray = $$$1.makeArray($$$1("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]"))); + var tabToggles = $$$1(Selector.DATA_TOGGLE); + + for (var i = 0; i < tabToggles.length; i++) { + var elem = tabToggles[i]; + var selector = Util.getSelectorFromElement(elem); + + if (selector !== null && $$$1(selector).filter(element).length > 0) { + this._selector = selector; + + this._triggerArray.push(elem); + } + } + + this._parent = this._config.parent ? this._getParent() : null; + + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._element, this._triggerArray); + } + + if (this._config.toggle) { + this.toggle(); + } + } // Getters + + + var _proto = Collapse.prototype; + + // Public + _proto.toggle = function toggle() { + if ($$$1(this._element).hasClass(ClassName.SHOW)) { + this.hide(); + } else { + this.show(); + } + }; + + _proto.show = function show() { + var _this = this; + + if (this._isTransitioning || $$$1(this._element).hasClass(ClassName.SHOW)) { + return; + } + + var actives; + var activesData; + + if (this._parent) { + actives = $$$1.makeArray($$$1(this._parent).find(Selector.ACTIVES).filter("[data-parent=\"" + this._config.parent + "\"]")); + + if (actives.length === 0) { + actives = null; + } + } + + if (actives) { + activesData = $$$1(actives).not(this._selector).data(DATA_KEY); + + if (activesData && activesData._isTransitioning) { + return; + } + } + + var startEvent = $$$1.Event(Event.SHOW); + $$$1(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + if (actives) { + Collapse._jQueryInterface.call($$$1(actives).not(this._selector), 'hide'); + + if (!activesData) { + $$$1(actives).data(DATA_KEY, null); + } + } + + var dimension = this._getDimension(); + + $$$1(this._element).removeClass(ClassName.COLLAPSE).addClass(ClassName.COLLAPSING); + this._element.style[dimension] = 0; + + if (this._triggerArray.length > 0) { + $$$1(this._triggerArray).removeClass(ClassName.COLLAPSED).attr('aria-expanded', true); + } + + this.setTransitioning(true); + + var complete = function complete() { + $$$1(_this._element).removeClass(ClassName.COLLAPSING).addClass(ClassName.COLLAPSE).addClass(ClassName.SHOW); + _this._element.style[dimension] = ''; + + _this.setTransitioning(false); + + $$$1(_this._element).trigger(Event.SHOWN); + }; + + if (!Util.supportsTransitionEnd()) { + complete(); + return; + } + + var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + var scrollSize = "scroll" + capitalizedDimension; + $$$1(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); + this._element.style[dimension] = this._element[scrollSize] + "px"; + }; + + _proto.hide = function hide() { + var _this2 = this; + + if (this._isTransitioning || !$$$1(this._element).hasClass(ClassName.SHOW)) { + return; + } + + var startEvent = $$$1.Event(Event.HIDE); + $$$1(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + var dimension = this._getDimension(); + + this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px"; + Util.reflow(this._element); + $$$1(this._element).addClass(ClassName.COLLAPSING).removeClass(ClassName.COLLAPSE).removeClass(ClassName.SHOW); + + if (this._triggerArray.length > 0) { + for (var i = 0; i < this._triggerArray.length; i++) { + var trigger = this._triggerArray[i]; + var selector = Util.getSelectorFromElement(trigger); + + if (selector !== null) { + var $elem = $$$1(selector); + + if (!$elem.hasClass(ClassName.SHOW)) { + $$$1(trigger).addClass(ClassName.COLLAPSED).attr('aria-expanded', false); + } + } + } + } + + this.setTransitioning(true); + + var complete = function complete() { + _this2.setTransitioning(false); + + $$$1(_this2._element).removeClass(ClassName.COLLAPSING).addClass(ClassName.COLLAPSE).trigger(Event.HIDDEN); + }; + + this._element.style[dimension] = ''; + + if (!Util.supportsTransitionEnd()) { + complete(); + return; + } + + $$$1(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); + }; + + _proto.setTransitioning = function setTransitioning(isTransitioning) { + this._isTransitioning = isTransitioning; + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + this._config = null; + this._parent = null; + this._element = null; + this._triggerArray = null; + this._isTransitioning = null; + }; // Private + + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, Default, config); + config.toggle = Boolean(config.toggle); // Coerce string values + + Util.typeCheckConfig(NAME, config, DefaultType); + return config; + }; + + _proto._getDimension = function _getDimension() { + var hasWidth = $$$1(this._element).hasClass(Dimension.WIDTH); + return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT; + }; + + _proto._getParent = function _getParent() { + var _this3 = this; + + var parent = null; + + if (Util.isElement(this._config.parent)) { + parent = this._config.parent; // It's a jQuery object + + if (typeof this._config.parent.jquery !== 'undefined') { + parent = this._config.parent[0]; + } + } else { + parent = $$$1(this._config.parent)[0]; + } + + var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]"; + $$$1(parent).find(selector).each(function (i, element) { + _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]); + }); + return parent; + }; + + _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) { + if (element) { + var isOpen = $$$1(element).hasClass(ClassName.SHOW); + + if (triggerArray.length > 0) { + $$$1(triggerArray).toggleClass(ClassName.COLLAPSED, !isOpen).attr('aria-expanded', isOpen); + } + } + }; // Static + + + Collapse._getTargetFromElement = function _getTargetFromElement(element) { + var selector = Util.getSelectorFromElement(element); + return selector ? $$$1(selector)[0] : null; + }; + + Collapse._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $this = $$$1(this); + var data = $this.data(DATA_KEY); + + var _config = _extends({}, Default, $this.data(), typeof config === 'object' && config); + + if (!data && _config.toggle && /show|hide/.test(config)) { + _config.toggle = false; + } + + if (!data) { + data = new Collapse(this, _config); + $this.data(DATA_KEY, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Collapse, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }]); + return Collapse; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.currentTarget.tagName === 'A') { + event.preventDefault(); + } + + var $trigger = $$$1(this); + var selector = Util.getSelectorFromElement(this); + $$$1(selector).each(function () { + var $target = $$$1(this); + var data = $target.data(DATA_KEY); + var config = data ? 'toggle' : $trigger.data(); + + Collapse._jQueryInterface.call($target, config); + }); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Collapse._jQueryInterface; + $$$1.fn[NAME].Constructor = Collapse; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Collapse._jQueryInterface; + }; + + return Collapse; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): dropdown.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Dropdown = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'dropdown'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.dropdown'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key + + var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key + + var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key + + var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key + + var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key + + var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse) + + var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE); + var Event = { + HIDE: "hide" + EVENT_KEY, + HIDDEN: "hidden" + EVENT_KEY, + SHOW: "show" + EVENT_KEY, + SHOWN: "shown" + EVENT_KEY, + CLICK: "click" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY, + KEYDOWN_DATA_API: "keydown" + EVENT_KEY + DATA_API_KEY, + KEYUP_DATA_API: "keyup" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + DISABLED: 'disabled', + SHOW: 'show', + DROPUP: 'dropup', + DROPRIGHT: 'dropright', + DROPLEFT: 'dropleft', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left', + POSITION_STATIC: 'position-static' + }; + var Selector = { + DATA_TOGGLE: '[data-toggle="dropdown"]', + FORM_CHILD: '.dropdown form', + MENU: '.dropdown-menu', + NAVBAR_NAV: '.navbar-nav', + VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled)' + }; + var AttachmentMap = { + TOP: 'top-start', + TOPEND: 'top-end', + BOTTOM: 'bottom-start', + BOTTOMEND: 'bottom-end', + RIGHT: 'right-start', + RIGHTEND: 'right-end', + LEFT: 'left-start', + LEFTEND: 'left-end' + }; + var Default = { + offset: 0, + flip: true, + boundary: 'scrollParent' + }; + var DefaultType = { + offset: '(number|string|function)', + flip: 'boolean', + boundary: '(string|element)' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Dropdown = + /*#__PURE__*/ + function () { + function Dropdown(element, config) { + this._element = element; + this._popper = null; + this._config = this._getConfig(config); + this._menu = this._getMenuElement(); + this._inNavbar = this._detectNavbar(); + + this._addEventListeners(); + } // Getters + + + var _proto = Dropdown.prototype; + + // Public + _proto.toggle = function toggle() { + if (this._element.disabled || $$$1(this._element).hasClass(ClassName.DISABLED)) { + return; + } + + var parent = Dropdown._getParentFromElement(this._element); + + var isActive = $$$1(this._menu).hasClass(ClassName.SHOW); + + Dropdown._clearMenus(); + + if (isActive) { + return; + } + + var relatedTarget = { + relatedTarget: this._element + }; + var showEvent = $$$1.Event(Event.SHOW, relatedTarget); + $$$1(parent).trigger(showEvent); + + if (showEvent.isDefaultPrevented()) { + return; + } // Disable totally Popper.js for Dropdown in Navbar + + + if (!this._inNavbar) { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap dropdown require Popper.js (https://popper.js.org)'); + } + + var element = this._element; // For dropup with alignment we use the parent as popper container + + if ($$$1(parent).hasClass(ClassName.DROPUP)) { + if ($$$1(this._menu).hasClass(ClassName.MENULEFT) || $$$1(this._menu).hasClass(ClassName.MENURIGHT)) { + element = parent; + } + } // If boundary is not `scrollParent`, then set position to `static` + // to allow the menu to "escape" the scroll parent's boundaries + // https://github.com/twbs/bootstrap/issues/24251 + + + if (this._config.boundary !== 'scrollParent') { + $$$1(parent).addClass(ClassName.POSITION_STATIC); + } + + this._popper = new Popper(element, this._menu, this._getPopperConfig()); + } // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + + if ('ontouchstart' in document.documentElement && $$$1(parent).closest(Selector.NAVBAR_NAV).length === 0) { + $$$1('body').children().on('mouseover', null, $$$1.noop); + } + + this._element.focus(); + + this._element.setAttribute('aria-expanded', true); + + $$$1(this._menu).toggleClass(ClassName.SHOW); + $$$1(parent).toggleClass(ClassName.SHOW).trigger($$$1.Event(Event.SHOWN, relatedTarget)); + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + $$$1(this._element).off(EVENT_KEY); + this._element = null; + this._menu = null; + + if (this._popper !== null) { + this._popper.destroy(); + + this._popper = null; + } + }; + + _proto.update = function update() { + this._inNavbar = this._detectNavbar(); + + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + }; // Private + + + _proto._addEventListeners = function _addEventListeners() { + var _this = this; + + $$$1(this._element).on(Event.CLICK, function (event) { + event.preventDefault(); + event.stopPropagation(); + + _this.toggle(); + }); + }; + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, this.constructor.Default, $$$1(this._element).data(), config); + Util.typeCheckConfig(NAME, config, this.constructor.DefaultType); + return config; + }; + + _proto._getMenuElement = function _getMenuElement() { + if (!this._menu) { + var parent = Dropdown._getParentFromElement(this._element); + + this._menu = $$$1(parent).find(Selector.MENU)[0]; + } + + return this._menu; + }; + + _proto._getPlacement = function _getPlacement() { + var $parentDropdown = $$$1(this._element).parent(); + var placement = AttachmentMap.BOTTOM; // Handle dropup + + if ($parentDropdown.hasClass(ClassName.DROPUP)) { + placement = AttachmentMap.TOP; + + if ($$$1(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.TOPEND; + } + } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) { + placement = AttachmentMap.RIGHT; + } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) { + placement = AttachmentMap.LEFT; + } else if ($$$1(this._menu).hasClass(ClassName.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND; + } + + return placement; + }; + + _proto._detectNavbar = function _detectNavbar() { + return $$$1(this._element).closest('.navbar').length > 0; + }; + + _proto._getPopperConfig = function _getPopperConfig() { + var _this2 = this; + + var offsetConf = {}; + + if (typeof this._config.offset === 'function') { + offsetConf.fn = function (data) { + data.offsets = _extends({}, data.offsets, _this2._config.offset(data.offsets) || {}); + return data; + }; + } else { + offsetConf.offset = this._config.offset; + } + + var popperConfig = { + placement: this._getPlacement(), + modifiers: { + offset: offsetConf, + flip: { + enabled: this._config.flip + }, + preventOverflow: { + boundariesElement: this._config.boundary + } + } + }; + return popperConfig; + }; // Static + + + Dropdown._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + var _config = typeof config === 'object' ? config : null; + + if (!data) { + data = new Dropdown(this, _config); + $$$1(this).data(DATA_KEY, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + Dropdown._clearMenus = function _clearMenus(event) { + if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { + return; + } + + var toggles = $$$1.makeArray($$$1(Selector.DATA_TOGGLE)); + + for (var i = 0; i < toggles.length; i++) { + var parent = Dropdown._getParentFromElement(toggles[i]); + + var context = $$$1(toggles[i]).data(DATA_KEY); + var relatedTarget = { + relatedTarget: toggles[i] + }; + + if (!context) { + continue; + } + + var dropdownMenu = context._menu; + + if (!$$$1(parent).hasClass(ClassName.SHOW)) { + continue; + } + + if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $$$1.contains(parent, event.target)) { + continue; + } + + var hideEvent = $$$1.Event(Event.HIDE, relatedTarget); + $$$1(parent).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + continue; + } // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + + if ('ontouchstart' in document.documentElement) { + $$$1('body').children().off('mouseover', null, $$$1.noop); + } + + toggles[i].setAttribute('aria-expanded', 'false'); + $$$1(dropdownMenu).removeClass(ClassName.SHOW); + $$$1(parent).removeClass(ClassName.SHOW).trigger($$$1.Event(Event.HIDDEN, relatedTarget)); + } + }; + + Dropdown._getParentFromElement = function _getParentFromElement(element) { + var parent; + var selector = Util.getSelectorFromElement(element); + + if (selector) { + parent = $$$1(selector)[0]; + } + + return parent || element.parentNode; + }; // eslint-disable-next-line complexity + + + Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) { + // If not input/textarea: + // - And not a key in REGEXP_KEYDOWN => not a dropdown command + // If input/textarea: + // - If space key => not a dropdown command + // - If key is other than escape + // - If key is not up or down => not a dropdown command + // - If trigger inside the menu => not a dropdown command + if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $$$1(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (this.disabled || $$$1(this).hasClass(ClassName.DISABLED)) { + return; + } + + var parent = Dropdown._getParentFromElement(this); + + var isActive = $$$1(parent).hasClass(ClassName.SHOW); + + if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { + if (event.which === ESCAPE_KEYCODE) { + var toggle = $$$1(parent).find(Selector.DATA_TOGGLE)[0]; + $$$1(toggle).trigger('focus'); + } + + $$$1(this).trigger('click'); + return; + } + + var items = $$$1(parent).find(Selector.VISIBLE_ITEMS).get(); + + if (items.length === 0) { + return; + } + + var index = items.indexOf(event.target); + + if (event.which === ARROW_UP_KEYCODE && index > 0) { + // Up + index--; + } + + if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { + // Down + index++; + } + + if (index < 0) { + index = 0; + } + + items[index].focus(); + }; + + _createClass(Dropdown, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType; + } + }]); + return Dropdown; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler).on(Event.CLICK_DATA_API + " " + Event.KEYUP_DATA_API, Dropdown._clearMenus).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault(); + event.stopPropagation(); + + Dropdown._jQueryInterface.call($$$1(this), 'toggle'); + }).on(Event.CLICK_DATA_API, Selector.FORM_CHILD, function (e) { + e.stopPropagation(); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Dropdown._jQueryInterface; + $$$1.fn[NAME].Constructor = Dropdown; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Dropdown._jQueryInterface; + }; + + return Dropdown; +}($, Popper); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): modal.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Modal = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'modal'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.modal'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var TRANSITION_DURATION = 300; + var BACKDROP_TRANSITION_DURATION = 150; + var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key + + var Default = { + backdrop: true, + keyboard: true, + focus: true, + show: true + }; + var DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + focus: 'boolean', + show: 'boolean' + }; + var Event = { + HIDE: "hide" + EVENT_KEY, + HIDDEN: "hidden" + EVENT_KEY, + SHOW: "show" + EVENT_KEY, + SHOWN: "shown" + EVENT_KEY, + FOCUSIN: "focusin" + EVENT_KEY, + RESIZE: "resize" + EVENT_KEY, + CLICK_DISMISS: "click.dismiss" + EVENT_KEY, + KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY, + MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY, + MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + SCROLLBAR_MEASURER: 'modal-scrollbar-measure', + BACKDROP: 'modal-backdrop', + OPEN: 'modal-open', + FADE: 'fade', + SHOW: 'show' + }; + var Selector = { + DIALOG: '.modal-dialog', + DATA_TOGGLE: '[data-toggle="modal"]', + DATA_DISMISS: '[data-dismiss="modal"]', + FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + STICKY_CONTENT: '.sticky-top', + NAVBAR_TOGGLER: '.navbar-toggler' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Modal = + /*#__PURE__*/ + function () { + function Modal(element, config) { + this._config = this._getConfig(config); + this._element = element; + this._dialog = $$$1(element).find(Selector.DIALOG)[0]; + this._backdrop = null; + this._isShown = false; + this._isBodyOverflowing = false; + this._ignoreBackdropClick = false; + this._originalBodyPadding = 0; + this._scrollbarWidth = 0; + } // Getters + + + var _proto = Modal.prototype; + + // Public + _proto.toggle = function toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget); + }; + + _proto.show = function show(relatedTarget) { + var _this = this; + + if (this._isTransitioning || this._isShown) { + return; + } + + if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE)) { + this._isTransitioning = true; + } + + var showEvent = $$$1.Event(Event.SHOW, { + relatedTarget: relatedTarget + }); + $$$1(this._element).trigger(showEvent); + + if (this._isShown || showEvent.isDefaultPrevented()) { + return; + } + + this._isShown = true; + + this._checkScrollbar(); + + this._setScrollbar(); + + this._adjustDialog(); + + $$$1(document.body).addClass(ClassName.OPEN); + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $$$1(this._element).on(Event.CLICK_DISMISS, Selector.DATA_DISMISS, function (event) { + return _this.hide(event); + }); + $$$1(this._dialog).on(Event.MOUSEDOWN_DISMISS, function () { + $$$1(_this._element).one(Event.MOUSEUP_DISMISS, function (event) { + if ($$$1(event.target).is(_this._element)) { + _this._ignoreBackdropClick = true; + } + }); + }); + + this._showBackdrop(function () { + return _this._showElement(relatedTarget); + }); + }; + + _proto.hide = function hide(event) { + var _this2 = this; + + if (event) { + event.preventDefault(); + } + + if (this._isTransitioning || !this._isShown) { + return; + } + + var hideEvent = $$$1.Event(Event.HIDE); + $$$1(this._element).trigger(hideEvent); + + if (!this._isShown || hideEvent.isDefaultPrevented()) { + return; + } + + this._isShown = false; + var transition = Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE); + + if (transition) { + this._isTransitioning = true; + } + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $$$1(document).off(Event.FOCUSIN); + $$$1(this._element).removeClass(ClassName.SHOW); + $$$1(this._element).off(Event.CLICK_DISMISS); + $$$1(this._dialog).off(Event.MOUSEDOWN_DISMISS); + + if (transition) { + $$$1(this._element).one(Util.TRANSITION_END, function (event) { + return _this2._hideModal(event); + }).emulateTransitionEnd(TRANSITION_DURATION); + } else { + this._hideModal(); + } + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + $$$1(window, document, this._element, this._backdrop).off(EVENT_KEY); + this._config = null; + this._element = null; + this._dialog = null; + this._backdrop = null; + this._isShown = null; + this._isBodyOverflowing = null; + this._ignoreBackdropClick = null; + this._scrollbarWidth = null; + }; + + _proto.handleUpdate = function handleUpdate() { + this._adjustDialog(); + }; // Private + + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, Default, config); + Util.typeCheckConfig(NAME, config, DefaultType); + return config; + }; + + _proto._showElement = function _showElement(relatedTarget) { + var _this3 = this; + + var transition = Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE); + + if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { + // Don't move modal's DOM position + document.body.appendChild(this._element); + } + + this._element.style.display = 'block'; + + this._element.removeAttribute('aria-hidden'); + + this._element.scrollTop = 0; + + if (transition) { + Util.reflow(this._element); + } + + $$$1(this._element).addClass(ClassName.SHOW); + + if (this._config.focus) { + this._enforceFocus(); + } + + var shownEvent = $$$1.Event(Event.SHOWN, { + relatedTarget: relatedTarget + }); + + var transitionComplete = function transitionComplete() { + if (_this3._config.focus) { + _this3._element.focus(); + } + + _this3._isTransitioning = false; + $$$1(_this3._element).trigger(shownEvent); + }; + + if (transition) { + $$$1(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(TRANSITION_DURATION); + } else { + transitionComplete(); + } + }; + + _proto._enforceFocus = function _enforceFocus() { + var _this4 = this; + + $$$1(document).off(Event.FOCUSIN) // Guard against infinite focus loop + .on(Event.FOCUSIN, function (event) { + if (document !== event.target && _this4._element !== event.target && $$$1(_this4._element).has(event.target).length === 0) { + _this4._element.focus(); + } + }); + }; + + _proto._setEscapeEvent = function _setEscapeEvent() { + var _this5 = this; + + if (this._isShown && this._config.keyboard) { + $$$1(this._element).on(Event.KEYDOWN_DISMISS, function (event) { + if (event.which === ESCAPE_KEYCODE) { + event.preventDefault(); + + _this5.hide(); + } + }); + } else if (!this._isShown) { + $$$1(this._element).off(Event.KEYDOWN_DISMISS); + } + }; + + _proto._setResizeEvent = function _setResizeEvent() { + var _this6 = this; + + if (this._isShown) { + $$$1(window).on(Event.RESIZE, function (event) { + return _this6.handleUpdate(event); + }); + } else { + $$$1(window).off(Event.RESIZE); + } + }; + + _proto._hideModal = function _hideModal() { + var _this7 = this; + + this._element.style.display = 'none'; + + this._element.setAttribute('aria-hidden', true); + + this._isTransitioning = false; + + this._showBackdrop(function () { + $$$1(document.body).removeClass(ClassName.OPEN); + + _this7._resetAdjustments(); + + _this7._resetScrollbar(); + + $$$1(_this7._element).trigger(Event.HIDDEN); + }); + }; + + _proto._removeBackdrop = function _removeBackdrop() { + if (this._backdrop) { + $$$1(this._backdrop).remove(); + this._backdrop = null; + } + }; + + _proto._showBackdrop = function _showBackdrop(callback) { + var _this8 = this; + + var animate = $$$1(this._element).hasClass(ClassName.FADE) ? ClassName.FADE : ''; + + if (this._isShown && this._config.backdrop) { + var doAnimate = Util.supportsTransitionEnd() && animate; + this._backdrop = document.createElement('div'); + this._backdrop.className = ClassName.BACKDROP; + + if (animate) { + $$$1(this._backdrop).addClass(animate); + } + + $$$1(this._backdrop).appendTo(document.body); + $$$1(this._element).on(Event.CLICK_DISMISS, function (event) { + if (_this8._ignoreBackdropClick) { + _this8._ignoreBackdropClick = false; + return; + } + + if (event.target !== event.currentTarget) { + return; + } + + if (_this8._config.backdrop === 'static') { + _this8._element.focus(); + } else { + _this8.hide(); + } + }); + + if (doAnimate) { + Util.reflow(this._backdrop); + } + + $$$1(this._backdrop).addClass(ClassName.SHOW); + + if (!callback) { + return; + } + + if (!doAnimate) { + callback(); + return; + } + + $$$1(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(BACKDROP_TRANSITION_DURATION); + } else if (!this._isShown && this._backdrop) { + $$$1(this._backdrop).removeClass(ClassName.SHOW); + + var callbackRemove = function callbackRemove() { + _this8._removeBackdrop(); + + if (callback) { + callback(); + } + }; + + if (Util.supportsTransitionEnd() && $$$1(this._element).hasClass(ClassName.FADE)) { + $$$1(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(BACKDROP_TRANSITION_DURATION); + } else { + callbackRemove(); + } + } else if (callback) { + callback(); + } + }; // ---------------------------------------------------------------------- + // the following methods are used to handle overflowing modals + // todo (fat): these should probably be refactored out of modal.js + // ---------------------------------------------------------------------- + + + _proto._adjustDialog = function _adjustDialog() { + var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; + + if (!this._isBodyOverflowing && isModalOverflowing) { + this._element.style.paddingLeft = this._scrollbarWidth + "px"; + } + + if (this._isBodyOverflowing && !isModalOverflowing) { + this._element.style.paddingRight = this._scrollbarWidth + "px"; + } + }; + + _proto._resetAdjustments = function _resetAdjustments() { + this._element.style.paddingLeft = ''; + this._element.style.paddingRight = ''; + }; + + _proto._checkScrollbar = function _checkScrollbar() { + var rect = document.body.getBoundingClientRect(); + this._isBodyOverflowing = rect.left + rect.right < window.innerWidth; + this._scrollbarWidth = this._getScrollbarWidth(); + }; + + _proto._setScrollbar = function _setScrollbar() { + var _this9 = this; + + if (this._isBodyOverflowing) { + // Note: DOMNode.style.paddingRight returns the actual value or '' if not set + // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set + // Adjust fixed content padding + $$$1(Selector.FIXED_CONTENT).each(function (index, element) { + var actualPadding = $$$1(element)[0].style.paddingRight; + var calculatedPadding = $$$1(element).css('padding-right'); + $$$1(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this9._scrollbarWidth + "px"); + }); // Adjust sticky content margin + + $$$1(Selector.STICKY_CONTENT).each(function (index, element) { + var actualMargin = $$$1(element)[0].style.marginRight; + var calculatedMargin = $$$1(element).css('margin-right'); + $$$1(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this9._scrollbarWidth + "px"); + }); // Adjust navbar-toggler margin + + $$$1(Selector.NAVBAR_TOGGLER).each(function (index, element) { + var actualMargin = $$$1(element)[0].style.marginRight; + var calculatedMargin = $$$1(element).css('margin-right'); + $$$1(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) + _this9._scrollbarWidth + "px"); + }); // Adjust body padding + + var actualPadding = document.body.style.paddingRight; + var calculatedPadding = $$$1('body').css('padding-right'); + $$$1('body').data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px"); + } + }; + + _proto._resetScrollbar = function _resetScrollbar() { + // Restore fixed content padding + $$$1(Selector.FIXED_CONTENT).each(function (index, element) { + var padding = $$$1(element).data('padding-right'); + + if (typeof padding !== 'undefined') { + $$$1(element).css('padding-right', padding).removeData('padding-right'); + } + }); // Restore sticky content and navbar-toggler margin + + $$$1(Selector.STICKY_CONTENT + ", " + Selector.NAVBAR_TOGGLER).each(function (index, element) { + var margin = $$$1(element).data('margin-right'); + + if (typeof margin !== 'undefined') { + $$$1(element).css('margin-right', margin).removeData('margin-right'); + } + }); // Restore body padding + + var padding = $$$1('body').data('padding-right'); + + if (typeof padding !== 'undefined') { + $$$1('body').css('padding-right', padding).removeData('padding-right'); + } + }; + + _proto._getScrollbarWidth = function _getScrollbarWidth() { + // thx d.walsh + var scrollDiv = document.createElement('div'); + scrollDiv.className = ClassName.SCROLLBAR_MEASURER; + document.body.appendChild(scrollDiv); + var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; + }; // Static + + + Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + var _config = _extends({}, Modal.Default, $$$1(this).data(), typeof config === 'object' && config); + + if (!data) { + data = new Modal(this, _config); + $$$1(this).data(DATA_KEY, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](relatedTarget); + } else if (_config.show) { + data.show(relatedTarget); + } + }); + }; + + _createClass(Modal, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }]); + return Modal; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $$$1(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + var _this10 = this; + + var target; + var selector = Util.getSelectorFromElement(this); + + if (selector) { + target = $$$1(selector)[0]; + } + + var config = $$$1(target).data(DATA_KEY) ? 'toggle' : _extends({}, $$$1(target).data(), $$$1(this).data()); + + if (this.tagName === 'A' || this.tagName === 'AREA') { + event.preventDefault(); + } + + var $target = $$$1(target).one(Event.SHOW, function (showEvent) { + if (showEvent.isDefaultPrevented()) { + // Only register focus restorer if modal will actually get shown + return; + } + + $target.one(Event.HIDDEN, function () { + if ($$$1(_this10).is(':visible')) { + _this10.focus(); + } + }); + }); + + Modal._jQueryInterface.call($$$1(target), config, this); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $$$1.fn[NAME] = Modal._jQueryInterface; + $$$1.fn[NAME].Constructor = Modal; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Modal._jQueryInterface; + }; + + return Modal; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Tooltip = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'tooltip'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.tooltip'; + var EVENT_KEY = "." + DATA_KEY; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var TRANSITION_DURATION = 150; + var CLASS_PREFIX = 'bs-tooltip'; + var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); + var DefaultType = { + animation: 'boolean', + template: 'string', + title: '(string|element|function)', + trigger: 'string', + delay: '(number|object)', + html: 'boolean', + selector: '(string|boolean)', + placement: '(string|function)', + offset: '(number|string)', + container: '(string|element|boolean)', + fallbackPlacement: '(string|array)', + boundary: '(string|element)' + }; + var AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' + }; + var Default = { + animation: true, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + selector: false, + placement: 'top', + offset: 0, + container: false, + fallbackPlacement: 'flip', + boundary: 'scrollParent' + }; + var HoverState = { + SHOW: 'show', + OUT: 'out' + }; + var Event = { + HIDE: "hide" + EVENT_KEY, + HIDDEN: "hidden" + EVENT_KEY, + SHOW: "show" + EVENT_KEY, + SHOWN: "shown" + EVENT_KEY, + INSERTED: "inserted" + EVENT_KEY, + CLICK: "click" + EVENT_KEY, + FOCUSIN: "focusin" + EVENT_KEY, + FOCUSOUT: "focusout" + EVENT_KEY, + MOUSEENTER: "mouseenter" + EVENT_KEY, + MOUSELEAVE: "mouseleave" + EVENT_KEY + }; + var ClassName = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector = { + TOOLTIP: '.tooltip', + TOOLTIP_INNER: '.tooltip-inner', + ARROW: '.arrow' + }; + var Trigger = { + HOVER: 'hover', + FOCUS: 'focus', + CLICK: 'click', + MANUAL: 'manual' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Tooltip = + /*#__PURE__*/ + function () { + function Tooltip(element, config) { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap tooltips require Popper.js (https://popper.js.org)'); + } // private + + + this._isEnabled = true; + this._timeout = 0; + this._hoverState = ''; + this._activeTrigger = {}; + this._popper = null; // Protected + + this.element = element; + this.config = this._getConfig(config); + this.tip = null; + + this._setListeners(); + } // Getters + + + var _proto = Tooltip.prototype; + + // Public + _proto.enable = function enable() { + this._isEnabled = true; + }; + + _proto.disable = function disable() { + this._isEnabled = false; + }; + + _proto.toggleEnabled = function toggleEnabled() { + this._isEnabled = !this._isEnabled; + }; + + _proto.toggle = function toggle(event) { + if (!this._isEnabled) { + return; + } + + if (event) { + var dataKey = this.constructor.DATA_KEY; + var context = $$$1(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $$$1(event.currentTarget).data(dataKey, context); + } + + context._activeTrigger.click = !context._activeTrigger.click; + + if (context._isWithActiveTrigger()) { + context._enter(null, context); + } else { + context._leave(null, context); + } + } else { + if ($$$1(this.getTipElement()).hasClass(ClassName.SHOW)) { + this._leave(null, this); + + return; + } + + this._enter(null, this); + } + }; + + _proto.dispose = function dispose() { + clearTimeout(this._timeout); + $$$1.removeData(this.element, this.constructor.DATA_KEY); + $$$1(this.element).off(this.constructor.EVENT_KEY); + $$$1(this.element).closest('.modal').off('hide.bs.modal'); + + if (this.tip) { + $$$1(this.tip).remove(); + } + + this._isEnabled = null; + this._timeout = null; + this._hoverState = null; + this._activeTrigger = null; + + if (this._popper !== null) { + this._popper.destroy(); + } + + this._popper = null; + this.element = null; + this.config = null; + this.tip = null; + }; + + _proto.show = function show() { + var _this = this; + + if ($$$1(this.element).css('display') === 'none') { + throw new Error('Please use show on visible elements'); + } + + var showEvent = $$$1.Event(this.constructor.Event.SHOW); + + if (this.isWithContent() && this._isEnabled) { + $$$1(this.element).trigger(showEvent); + var isInTheDom = $$$1.contains(this.element.ownerDocument.documentElement, this.element); + + if (showEvent.isDefaultPrevented() || !isInTheDom) { + return; + } + + var tip = this.getTipElement(); + var tipId = Util.getUID(this.constructor.NAME); + tip.setAttribute('id', tipId); + this.element.setAttribute('aria-describedby', tipId); + this.setContent(); + + if (this.config.animation) { + $$$1(tip).addClass(ClassName.FADE); + } + + var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; + + var attachment = this._getAttachment(placement); + + this.addAttachmentClass(attachment); + var container = this.config.container === false ? document.body : $$$1(this.config.container); + $$$1(tip).data(this.constructor.DATA_KEY, this); + + if (!$$$1.contains(this.element.ownerDocument.documentElement, this.tip)) { + $$$1(tip).appendTo(container); + } + + $$$1(this.element).trigger(this.constructor.Event.INSERTED); + this._popper = new Popper(this.element, tip, { + placement: attachment, + modifiers: { + offset: { + offset: this.config.offset + }, + flip: { + behavior: this.config.fallbackPlacement + }, + arrow: { + element: Selector.ARROW + }, + preventOverflow: { + boundariesElement: this.config.boundary + } + }, + onCreate: function onCreate(data) { + if (data.originalPlacement !== data.placement) { + _this._handlePopperPlacementChange(data); + } + }, + onUpdate: function onUpdate(data) { + _this._handlePopperPlacementChange(data); + } + }); + $$$1(tip).addClass(ClassName.SHOW); // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + if ('ontouchstart' in document.documentElement) { + $$$1('body').children().on('mouseover', null, $$$1.noop); + } + + var complete = function complete() { + if (_this.config.animation) { + _this._fixTransition(); + } + + var prevHoverState = _this._hoverState; + _this._hoverState = null; + $$$1(_this.element).trigger(_this.constructor.Event.SHOWN); + + if (prevHoverState === HoverState.OUT) { + _this._leave(null, _this); + } + }; + + if (Util.supportsTransitionEnd() && $$$1(this.tip).hasClass(ClassName.FADE)) { + $$$1(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(Tooltip._TRANSITION_DURATION); + } else { + complete(); + } + } + }; + + _proto.hide = function hide(callback) { + var _this2 = this; + + var tip = this.getTipElement(); + var hideEvent = $$$1.Event(this.constructor.Event.HIDE); + + var complete = function complete() { + if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) { + tip.parentNode.removeChild(tip); + } + + _this2._cleanTipClass(); + + _this2.element.removeAttribute('aria-describedby'); + + $$$1(_this2.element).trigger(_this2.constructor.Event.HIDDEN); + + if (_this2._popper !== null) { + _this2._popper.destroy(); + } + + if (callback) { + callback(); + } + }; + + $$$1(this.element).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + return; + } + + $$$1(tip).removeClass(ClassName.SHOW); // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + if ('ontouchstart' in document.documentElement) { + $$$1('body').children().off('mouseover', null, $$$1.noop); + } + + this._activeTrigger[Trigger.CLICK] = false; + this._activeTrigger[Trigger.FOCUS] = false; + this._activeTrigger[Trigger.HOVER] = false; + + if (Util.supportsTransitionEnd() && $$$1(this.tip).hasClass(ClassName.FADE)) { + $$$1(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(TRANSITION_DURATION); + } else { + complete(); + } + + this._hoverState = ''; + }; + + _proto.update = function update() { + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + }; // Protected + + + _proto.isWithContent = function isWithContent() { + return Boolean(this.getTitle()); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $$$1(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $$$1(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var $tip = $$$1(this.getTipElement()); + this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle()); + $tip.removeClass(ClassName.FADE + " " + ClassName.SHOW); + }; + + _proto.setElementContent = function setElementContent($element, content) { + var html = this.config.html; + + if (typeof content === 'object' && (content.nodeType || content.jquery)) { + // Content is a DOM node or a jQuery + if (html) { + if (!$$$1(content).parent().is($element)) { + $element.empty().append(content); + } + } else { + $element.text($$$1(content).text()); + } + } else { + $element[html ? 'html' : 'text'](content); + } + }; + + _proto.getTitle = function getTitle() { + var title = this.element.getAttribute('data-original-title'); + + if (!title) { + title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title; + } + + return title; + }; // Private + + + _proto._getAttachment = function _getAttachment(placement) { + return AttachmentMap[placement.toUpperCase()]; + }; + + _proto._setListeners = function _setListeners() { + var _this3 = this; + + var triggers = this.config.trigger.split(' '); + triggers.forEach(function (trigger) { + if (trigger === 'click') { + $$$1(_this3.element).on(_this3.constructor.Event.CLICK, _this3.config.selector, function (event) { + return _this3.toggle(event); + }); + } else if (trigger !== Trigger.MANUAL) { + var eventIn = trigger === Trigger.HOVER ? _this3.constructor.Event.MOUSEENTER : _this3.constructor.Event.FOCUSIN; + var eventOut = trigger === Trigger.HOVER ? _this3.constructor.Event.MOUSELEAVE : _this3.constructor.Event.FOCUSOUT; + $$$1(_this3.element).on(eventIn, _this3.config.selector, function (event) { + return _this3._enter(event); + }).on(eventOut, _this3.config.selector, function (event) { + return _this3._leave(event); + }); + } + + $$$1(_this3.element).closest('.modal').on('hide.bs.modal', function () { + return _this3.hide(); + }); + }); + + if (this.config.selector) { + this.config = _extends({}, this.config, { + trigger: 'manual', + selector: '' + }); + } else { + this._fixTitle(); + } + }; + + _proto._fixTitle = function _fixTitle() { + var titleType = typeof this.element.getAttribute('data-original-title'); + + if (this.element.getAttribute('title') || titleType !== 'string') { + this.element.setAttribute('data-original-title', this.element.getAttribute('title') || ''); + this.element.setAttribute('title', ''); + } + }; + + _proto._enter = function _enter(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $$$1(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $$$1(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true; + } + + if ($$$1(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) { + context._hoverState = HoverState.SHOW; + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.SHOW; + + if (!context.config.delay || !context.config.delay.show) { + context.show(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.SHOW) { + context.show(); + } + }, context.config.delay.show); + }; + + _proto._leave = function _leave(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $$$1(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $$$1(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false; + } + + if (context._isWithActiveTrigger()) { + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.OUT; + + if (!context.config.delay || !context.config.delay.hide) { + context.hide(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.OUT) { + context.hide(); + } + }, context.config.delay.hide); + }; + + _proto._isWithActiveTrigger = function _isWithActiveTrigger() { + for (var trigger in this._activeTrigger) { + if (this._activeTrigger[trigger]) { + return true; + } + } + + return false; + }; + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, this.constructor.Default, $$$1(this.element).data(), config); + + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + + if (typeof config.title === 'number') { + config.title = config.title.toString(); + } + + if (typeof config.content === 'number') { + config.content = config.content.toString(); + } + + Util.typeCheckConfig(NAME, config, this.constructor.DefaultType); + return config; + }; + + _proto._getDelegateConfig = function _getDelegateConfig() { + var config = {}; + + if (this.config) { + for (var key in this.config) { + if (this.constructor.Default[key] !== this.config[key]) { + config[key] = this.config[key]; + } + } + } + + return config; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $$$1(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + }; + + _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(data) { + this._cleanTipClass(); + + this.addAttachmentClass(this._getAttachment(data.placement)); + }; + + _proto._fixTransition = function _fixTransition() { + var tip = this.getTipElement(); + var initConfigAnimation = this.config.animation; + + if (tip.getAttribute('x-placement') !== null) { + return; + } + + $$$1(tip).removeClass(ClassName.FADE); + this.config.animation = false; + this.hide(); + this.show(); + this.config.animation = initConfigAnimation; + }; // Static + + + Tooltip._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + var _config = typeof config === 'object' && config; + + if (!data && /dispose|hide/.test(config)) { + return; + } + + if (!data) { + data = new Tooltip(this, _config); + $$$1(this).data(DATA_KEY, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Tooltip, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }, { + key: "NAME", + get: function get() { + return NAME; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY; + } + }, { + key: "Event", + get: function get() { + return Event; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType; + } + }]); + return Tooltip; + }(); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $$$1.fn[NAME] = Tooltip._jQueryInterface; + $$$1.fn[NAME].Constructor = Tooltip; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Tooltip._jQueryInterface; + }; + + return Tooltip; +}($, Popper); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var Popover = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'popover'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.popover'; + var EVENT_KEY = "." + DATA_KEY; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var CLASS_PREFIX = 'bs-popover'; + var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); + var Default = _extends({}, Tooltip.Default, { + placement: 'right', + trigger: 'click', + content: '', + template: '' + }); + var DefaultType = _extends({}, Tooltip.DefaultType, { + content: '(string|element|function)' + }); + var ClassName = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector = { + TITLE: '.popover-header', + CONTENT: '.popover-body' + }; + var Event = { + HIDE: "hide" + EVENT_KEY, + HIDDEN: "hidden" + EVENT_KEY, + SHOW: "show" + EVENT_KEY, + SHOWN: "shown" + EVENT_KEY, + INSERTED: "inserted" + EVENT_KEY, + CLICK: "click" + EVENT_KEY, + FOCUSIN: "focusin" + EVENT_KEY, + FOCUSOUT: "focusout" + EVENT_KEY, + MOUSEENTER: "mouseenter" + EVENT_KEY, + MOUSELEAVE: "mouseleave" + EVENT_KEY + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var Popover = + /*#__PURE__*/ + function (_Tooltip) { + _inheritsLoose(Popover, _Tooltip); + + function Popover() { + return _Tooltip.apply(this, arguments) || this; + } + + var _proto = Popover.prototype; + + // Overrides + _proto.isWithContent = function isWithContent() { + return this.getTitle() || this._getContent(); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $$$1(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $$$1(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var $tip = $$$1(this.getTipElement()); // We use append for html objects to maintain js events + + this.setElementContent($tip.find(Selector.TITLE), this.getTitle()); + + var content = this._getContent(); + + if (typeof content === 'function') { + content = content.call(this.element); + } + + this.setElementContent($tip.find(Selector.CONTENT), content); + $tip.removeClass(ClassName.FADE + " " + ClassName.SHOW); + }; // Private + + + _proto._getContent = function _getContent() { + return this.element.getAttribute('data-content') || this.config.content; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $$$1(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + }; // Static + + + Popover._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $$$1(this).data(DATA_KEY); + + var _config = typeof config === 'object' ? config : null; + + if (!data && /destroy|hide/.test(config)) { + return; + } + + if (!data) { + data = new Popover(this, _config); + $$$1(this).data(DATA_KEY, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Popover, null, [{ + key: "VERSION", + // Getters + get: function get() { + return VERSION; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }, { + key: "NAME", + get: function get() { + return NAME; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY; + } + }, { + key: "Event", + get: function get() { + return Event; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType; + } + }]); + return Popover; + }(Tooltip); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $$$1.fn[NAME] = Popover._jQueryInterface; + $$$1.fn[NAME].Constructor = Popover; + + $$$1.fn[NAME].noConflict = function () { + $$$1.fn[NAME] = JQUERY_NO_CONFLICT; + return Popover._jQueryInterface; + }; + + return Popover; +}($); + +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.0.0): scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +var ScrollSpy = function ($$$1) { + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + var NAME = 'scrollspy'; + var VERSION = '4.0.0'; + var DATA_KEY = 'bs.scrollspy'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $$$1.fn[NAME]; + var Default = { + offset: 10, + method: 'auto', + target: '' + }; + var DefaultType = { + offset: 'number', + method: 'string', + target: '(string|element)' + }; + var Event = { + ACTIVATE: "activate" + EVENT_KEY, + SCROLL: "scroll" + EVENT_KEY, + LOAD_DATA_API: "load" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + DROPDOWN_ITEM: 'dropdown-item', + DROPDOWN_MENU: 'dropdown-menu', + ACTIVE: 'active' + }; + var Selector = { + DATA_SPY: '[data-spy="scroll"]', + ACTIVE: '.active', + NAV_LIST_GROUP: '.nav, .list-group', + NAV_LINKS: '.nav-link', + NAV_ITEMS: '.nav-item', + LIST_ITEMS: '.list-group-item', + DROPDOWN: '.dropdown', + DROPDOWN_ITEMS: '.dropdown-item', + DROPDOWN_TOGGLE: '.dropdown-toggle' + }; + var OffsetMethod = { + OFFSET: 'offset', + POSITION: 'position' + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + }; + + var ScrollSpy = + /*#__PURE__*/ + function () { + function ScrollSpy(element, config) { + var _this = this; + + this._element = element; + this._scrollElement = element.tagName === 'BODY' ? window : element; + this._config = this._getConfig(config); + this._selector = this._config.target + " " + Selector.NAV_LINKS + "," + (this._config.target + " " + Selector.LIST_ITEMS + ",") + (this._config.target + " " + Selector.DROPDOWN_ITEMS); + this._offsets = []; + this._targets = []; + this._activeTarget = null; + this._scrollHeight = 0; + $$$1(this._scrollElement).on(Event.SCROLL, function (event) { + return _this._process(event); + }); + this.refresh(); + + this._process(); + } // Getters + + + var _proto = ScrollSpy.prototype; + + // Public + _proto.refresh = function refresh() { + var _this2 = this; + + var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION; + var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; + var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0; + this._offsets = []; + this._targets = []; + this._scrollHeight = this._getScrollHeight(); + var targets = $$$1.makeArray($$$1(this._selector)); + targets.map(function (element) { + var target; + var targetSelector = Util.getSelectorFromElement(element); + + if (targetSelector) { + target = $$$1(targetSelector)[0]; + } + + if (target) { + var targetBCR = target.getBoundingClientRect(); + + if (targetBCR.width || targetBCR.height) { + // TODO (fat): remove sketch reliance on jQuery position/offset + return [$$$1(target)[offsetMethod]().top + offsetBase, targetSelector]; + } + } + + return null; + }).filter(function (item) { + return item; + }).sort(function (a, b) { + return a[0] - b[0]; + }).forEach(function (item) { + _this2._offsets.push(item[0]); + + _this2._targets.push(item[1]); + }); + }; + + _proto.dispose = function dispose() { + $$$1.removeData(this._element, DATA_KEY); + $$$1(this._scrollElement).off(EVENT_KEY); + this._element = null; + this._scrollElement = null; + this._config = null; + this._selector = null; + this._offsets = null; + this._targets = null; + this._activeTarget = null; + this._scrollHeight = null; + }; // Private + + + _proto._getConfig = function _getConfig(config) { + config = _extends({}, Default, config); + + if (typeof config.target !== 'string') { + var id = $$$1(config.target).attr('id'); + + if (!id) { + id = Util.getUID(NAME); + $$$1(config.target).attr('id', id); + } + + config.target = "#" + id; + } + + Util.typeCheckConfig(NAME, config, DefaultType); + return config; + }; + + _proto._getScrollTop = function _getScrollTop() { + return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; + }; + + _proto._getScrollHeight = function _getScrollHeight() { + return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); + }; + + _proto._getOffsetHeight = function _getOffsetHeight() { + return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; + }; + + _proto._process = function _process() { + var scrollTop = this._getScrollTop() + this._config.offset; + + var scrollHeight = this._getScrollHeight(); + + var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); + + if (this._scrollHeight !== scrollHeight) { + this.refresh(); + } + + if (scrollTop >= maxScroll) { + var target = this._targets[this._targets.length - 1]; + + if (this._activeTarget !== target) { + this._activate(target); + } + + return; + } + + if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { + this._activeTarget = null; + + this._clear(); + + return; + } + + for (var i = this._offsets.length; i--;) { + var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); + + if (isActiveTarget) { + this._activate(this._targets[i]); + } + } + }; + + _proto._activate = function _activate(target) { + this._activeTarget = target; + + this._clear(); + + var queries = this._selector.split(','); // eslint-disable-next-line arrow-body-style + + + queries = queries.map(function (selector) { + return selector + "[data-target=\"" + target + "\"]," + (selector + "[href=\"" + target + "\"]"); + }); + var $link = $$$1(queries.join(',')); + + if ($link.hasClass(ClassName.DROPDOWN_ITEM)) { + $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE); + $link.addClass(ClassName.ACTIVE); + } else { + // Set triggered link as active + $link.addClass(ClassName.ACTIVE); // Set triggered links parents as active + // With both
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},f="show",d="out",_={HIDE:"hide"+o,HIDDEN:"hidden"+o,SHOW:"show"+o,SHOWN:"shown"+o,INSERTED:"inserted"+o,CLICK:"click"+o,FOCUSIN:"focusin"+o,FOCUSOUT:"focusout"+o,MOUSEENTER:"mouseenter"+o,MOUSELEAVE:"mouseleave"+o},g="fade",p="show",m=".tooltip-inner",v=".arrow",E="hover",T="focus",y="click",C="manual",I=function(){function a(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var I=a.prototype;return I.enable=function(){this._isEnabled=!0},I.disable=function(){this._isEnabled=!1},I.toggleEnabled=function(){this._isEnabled=!this._isEnabled},I.toggle=function(e){if(this._isEnabled)if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(p))return void this._leave(null,this);this._enter(null,this)}},I.dispose=function(){clearTimeout(this._timeout),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},I.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var i=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){t(this.element).trigger(i);var s=t.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),o=P.getUID(this.constructor.NAME);r.setAttribute("id",o),this.element.setAttribute("aria-describedby",o),this.setContent(),this.config.animation&&t(r).addClass(g);var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var c=!1===this.config.container?document.body:t(this.config.container);t(r).data(this.constructor.DATA_KEY,this),t.contains(this.element.ownerDocument.documentElement,this.tip)||t(r).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,{placement:h,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:v},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),t(r).addClass(p),"ontouchstart"in document.documentElement&&t("body").children().on("mouseover",null,t.noop);var u=function(){e.config.animation&&e._fixTransition();var n=e._hoverState;e._hoverState=null,t(e.element).trigger(e.constructor.Event.SHOWN),n===d&&e._leave(null,e)};P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(this.tip).one(P.TRANSITION_END,u).emulateTransitionEnd(a._TRANSITION_DURATION):u()}},I.hide=function(e){var n=this,i=this.getTipElement(),s=t.Event(this.constructor.Event.HIDE),r=function(){n._hoverState!==f&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()};t(this.element).trigger(s),s.isDefaultPrevented()||(t(i).removeClass(p),"ontouchstart"in document.documentElement&&t("body").children().off("mouseover",null,t.noop),this._activeTrigger[y]=!1,this._activeTrigger[T]=!1,this._activeTrigger[E]=!1,P.supportsTransitionEnd()&&t(this.tip).hasClass(g)?t(i).one(P.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},I.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},I.isWithContent=function(){return Boolean(this.getTitle())},I.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-tooltip-"+e)},I.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},I.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(m),this.getTitle()),e.removeClass(g+" "+p)},I.setElementContent=function(e,n){var i=this.config.html;"object"==typeof n&&(n.nodeType||n.jquery)?i?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[i?"html":"text"](n)},I.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},I._getAttachment=function(t){return c[t.toUpperCase()]},I._setListeners=function(){var e=this;this.config.trigger.split(" ").forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==C){var i=n===E?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,s=n===E?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(s,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},I._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},I._enter=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T:E]=!0),t(n.getTipElement()).hasClass(p)||n._hoverState===f?n._hoverState=f:(clearTimeout(n._timeout),n._hoverState=f,n.config.delay&&n.config.delay.show?n._timeout=setTimeout(function(){n._hoverState===f&&n.show()},n.config.delay.show):n.show())},I._leave=function(e,n){var i=this.constructor.DATA_KEY;(n=n||t(e.currentTarget).data(i))||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T:E]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState=d,n.config.delay&&n.config.delay.hide?n._timeout=setTimeout(function(){n._hoverState===d&&n.hide()},n.config.delay.hide):n.hide())},I._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},I._getConfig=function(n){return"number"==typeof(n=r({},this.constructor.Default,t(this.element).data(),n)).delay&&(n.delay={show:n.delay,hide:n.delay}),"number"==typeof n.title&&(n.title=n.title.toString()),"number"==typeof n.content&&(n.content=n.content.toString()),P.typeCheckConfig(e,n,this.constructor.DefaultType),n},I._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},I._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(l);null!==n&&n.length>0&&e.removeClass(n.join(""))},I._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},I._fixTransition=function(){var e=this.getTipElement(),n=this.config.animation;null===e.getAttribute("x-placement")&&(t(e).removeClass(g),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},a._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(i),s="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new a(this,s),t(this).data(i,n)),"string"==typeof e)){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},s(a,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return u}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return i}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return o}},{key:"DefaultType",get:function(){return h}}]),a}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=a,I._jQueryInterface},I}(e),x=function(t){var e="popover",n="bs.popover",i="."+n,o=t.fn[e],a=new RegExp("(^|\\s)bs-popover\\S+","g"),l=r({},U.Default,{placement:"right",trigger:"click",content:"",template:''}),h=r({},U.DefaultType,{content:"(string|element|function)"}),c="fade",u="show",f=".popover-header",d=".popover-body",_={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},g=function(r){var o,g;function p(){return r.apply(this,arguments)||this}g=r,(o=p).prototype=Object.create(g.prototype),o.prototype.constructor=o,o.__proto__=g;var m=p.prototype;return m.isWithContent=function(){return this.getTitle()||this._getContent()},m.addAttachmentClass=function(e){t(this.getTipElement()).addClass("bs-popover-"+e)},m.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0],this.tip},m.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(f),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(e.find(d),n),e.removeClass(c+" "+u)},m._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},m._cleanTipClass=function(){var e=t(this.getTipElement()),n=e.attr("class").match(a);null!==n&&n.length>0&&e.removeClass(n.join(""))},p._jQueryInterface=function(e){return this.each(function(){var i=t(this).data(n),s="object"==typeof e?e:null;if((i||!/destroy|hide/.test(e))&&(i||(i=new p(this,s),t(this).data(n,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},s(p,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return l}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return n}},{key:"Event",get:function(){return _}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return h}}]),p}(U);return t.fn[e]=g._jQueryInterface,t.fn[e].Constructor=g,t.fn[e].noConflict=function(){return t.fn[e]=o,g._jQueryInterface},g}(e),K=function(t){var e="scrollspy",n="bs.scrollspy",i="."+n,o=t.fn[e],a={offset:10,method:"auto",target:""},l={offset:"number",method:"string",target:"(string|element)"},h={ACTIVATE:"activate"+i,SCROLL:"scroll"+i,LOAD_DATA_API:"load"+i+".data-api"},c="dropdown-item",u="active",f={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},d="offset",_="position",g=function(){function o(e,n){var i=this;this._element=e,this._scrollElement="BODY"===e.tagName?window:e,this._config=this._getConfig(n),this._selector=this._config.target+" "+f.NAV_LINKS+","+this._config.target+" "+f.LIST_ITEMS+","+this._config.target+" "+f.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,t(this._scrollElement).on(h.SCROLL,function(t){return i._process(t)}),this.refresh(),this._process()}var g=o.prototype;return g.refresh=function(){var e=this,n=this._scrollElement===this._scrollElement.window?d:_,i="auto"===this._config.method?n:this._config.method,s=i===_?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),t.makeArray(t(this._selector)).map(function(e){var n,r=P.getSelectorFromElement(e);if(r&&(n=t(r)[0]),n){var o=n.getBoundingClientRect();if(o.width||o.height)return[t(n)[i]().top+s,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(t){e._offsets.push(t[0]),e._targets.push(t[1])})},g.dispose=function(){t.removeData(this._element,n),t(this._scrollElement).off(i),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},g._getConfig=function(n){if("string"!=typeof(n=r({},a,n)).target){var i=t(n.target).attr("id");i||(i=P.getUID(e),t(n.target).attr("id",i)),n.target="#"+i}return P.typeCheckConfig(e,n,l),n},g._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},g._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},g._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},g._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var s=this._offsets.length;s--;){this._activeTarget!==this._targets[s]&&t>=this._offsets[s]&&("undefined"==typeof this._offsets[s+1]||t=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=P,t.Alert=L,t.Button=R,t.Carousel=j,t.Collapse=H,t.Dropdown=W,t.Modal=M,t.Popover=x,t.Scrollspy=K,t.Tab=V,t.Tooltip=U,Object.defineProperty(t,"__esModule",{value:!0})}); +//# sourceMappingURL=bootstrap.min.js.map \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.min.js.map b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.min.js.map new file mode 100644 index 000000000..a2100fa39 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/js/bootstrap/bootstrap.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../rollupPluginBabelHelpers","../../js/src/util.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/carousel.js","../../js/src/collapse.js","../../js/src/dropdown.js","../../js/src/modal.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/index.js"],"names":["_defineProperties","target","props","i","length","descriptor","enumerable","configurable","writable","Object","defineProperty","key","_createClass","Constructor","protoProps","staticProps","prototype","_extends","assign","arguments","source","hasOwnProperty","call","apply","this","$","NAME","DATA_KEY","EVENT_KEY","JQUERY_NO_CONFLICT","Event","ClassName","Alert","DATA_API_KEY","Selector","Button","Util","transition","transitionEndEmulator","duration","called","one","TRANSITION_END","triggerTransitionEnd","_this","prefix","Math","random","document","getElementById","element","selector","getAttribute","charAt","escapeSelector","substr","replace","find","err","offsetHeight","trigger","end","Boolean","obj","nodeType","componentName","config","configTypes","property","expectedTypes","value","valueType","isElement","toString","match","toLowerCase","RegExp","test","Error","toUpperCase","window","QUnit","fn","emulateTransitionEnd","supportsTransitionEnd","event","special","is","handleObj","handler","_element","close","rootElement","_getRootElement","_triggerCloseEvent","isDefaultPrevented","_removeElement","dispose","removeData","getSelectorFromElement","parent","closest","closeEvent","CLOSE","removeClass","hasClass","_destroyElement","detach","CLOSED","remove","_jQueryInterface","each","$element","data","_handleDismiss","alertInstance","preventDefault","on","CLICK_DATA_API","noConflict","toggle","triggerChangeEvent","addAriaPressed","input","type","checked","activeElement","hasAttribute","classList","contains","focus","setAttribute","toggleClass","button","FOCUS_BLUR_DATA_API","Carousel","Default","DefaultType","Direction","_items","_interval","_activeElement","_isPaused","_isSliding","touchTimeout","_config","_getConfig","_indicatorsElement","INDICATORS","_addEventListeners","next","_slide","nextWhenVisible","hidden","css","prev","pause","NEXT_PREV","cycle","interval","setInterval","visibilityState","bind","to","index","ACTIVE_ITEM","activeIndex","_getItemIndex","SLID","direction","off","typeCheckConfig","keyboard","KEYDOWN","_this2","_keydown","MOUSEENTER","MOUSELEAVE","documentElement","TOUCHEND","setTimeout","tagName","which","makeArray","ITEM","indexOf","_getItemByDirection","isNextDirection","isPrevDirection","lastItemIndex","wrap","itemIndex","_triggerSlideEvent","relatedTarget","eventDirectionName","targetIndex","fromIndex","slideEvent","SLIDE","_setActiveIndicatorElement","ACTIVE","nextIndicator","children","addClass","directionalClassName","orderClassName","activeElementIndex","nextElement","nextElementIndex","isCycling","slidEvent","reflow","_this3","action","slide","TypeError","_dataApiClickHandler","slideIndex","DATA_SLIDE","LOAD_DATA_API","DATA_RIDE","$carousel","Collapse","Dimension","_isTransitioning","_triggerArray","id","tabToggles","DATA_TOGGLE","elem","filter","_selector","push","_parent","_getParent","_addAriaAndCollapsedClass","hide","show","actives","activesData","ACTIVES","not","startEvent","SHOW","dimension","_getDimension","style","attr","setTransitioning","complete","SHOWN","scrollSize","slice","HIDE","getBoundingClientRect","HIDDEN","isTransitioning","jquery","_getTargetFromElement","triggerArray","isOpen","$this","currentTarget","$trigger","$target","Dropdown","REGEXP_KEYDOWN","ARROW_UP_KEYCODE","AttachmentMap","_popper","_menu","_getMenuElement","_inNavbar","_detectNavbar","disabled","_getParentFromElement","isActive","_clearMenus","showEvent","Popper","boundary","_getPopperConfig","noop","destroy","update","scheduleUpdate","CLICK","stopPropagation","constructor","_getPlacement","$parentDropdown","placement","offsetConf","offset","offsets","flip","toggles","context","dropdownMenu","hideEvent","parentNode","_dataApiKeydownHandler","items","get","KEYDOWN_DATA_API","KEYUP_DATA_API","e","Modal","_dialog","DIALOG","_backdrop","_isShown","_isBodyOverflowing","_ignoreBackdropClick","_originalBodyPadding","_scrollbarWidth","_checkScrollbar","_setScrollbar","_adjustDialog","body","_setEscapeEvent","_setResizeEvent","CLICK_DISMISS","DATA_DISMISS","MOUSEDOWN_DISMISS","MOUSEUP_DISMISS","_showBackdrop","_showElement","FOCUSIN","_hideModal","handleUpdate","Node","ELEMENT_NODE","appendChild","display","removeAttribute","scrollTop","_enforceFocus","shownEvent","transitionComplete","_this4","has","KEYDOWN_DISMISS","RESIZE","_this6","_resetAdjustments","_resetScrollbar","_this7","_removeBackdrop","callback","animate","backdrop","doAnimate","createElement","className","appendTo","_this8","callbackRemove","isModalOverflowing","scrollHeight","clientHeight","paddingLeft","paddingRight","rect","left","right","innerWidth","_getScrollbarWidth","FIXED_CONTENT","actualPadding","calculatedPadding","parseFloat","_this9","STICKY_CONTENT","actualMargin","marginRight","calculatedMargin","NAVBAR_TOGGLER","padding","margin","scrollDiv","scrollbarWidth","width","clientWidth","removeChild","Tooltip","BSCLS_PREFIX_REGEX","HoverState","Trigger","_isEnabled","_timeout","_hoverState","_activeTrigger","tip","_setListeners","enable","disable","toggleEnabled","dataKey","_getDelegateConfig","click","_isWithActiveTrigger","_enter","_leave","getTipElement","isWithContent","isInTheDom","ownerDocument","tipId","getUID","setContent","animation","attachment","_getAttachment","addAttachmentClass","container","INSERTED","fallbackPlacement","originalPlacement","_handlePopperPlacementChange","_fixTransition","prevHoverState","_TRANSITION_DURATION","_cleanTipClass","getTitle","CLASS_PREFIX","template","$tip","setElementContent","content","html","empty","append","text","title","split","forEach","eventIn","eventOut","FOCUSOUT","_fixTitle","titleType","delay","tabClass","join","initConfigAnimation","Popover","subClass","superClass","create","__proto__","_getContent","ScrollSpy","OffsetMethod","_scrollElement","NAV_LINKS","LIST_ITEMS","DROPDOWN_ITEMS","_offsets","_targets","_activeTarget","_scrollHeight","SCROLL","_process","refresh","autoMethod","offsetMethod","method","offsetBase","_getScrollTop","_getScrollHeight","map","targetSelector","targetBCR","height","top","item","sort","a","b","pageYOffset","max","_getOffsetHeight","innerHeight","maxScroll","_activate","_clear","queries","$link","DROPDOWN","DROPDOWN_TOGGLE","parents","NAV_LIST_GROUP","NAV_ITEMS","ACTIVATE","scrollSpys","DATA_SPY","$spy","Tab","previous","listElement","itemSelector","nodeName","hiddenEvent","active","_transitionComplete","dropdownChild","dropdownElement","version"],"mappings":"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","sourcesContent":["export { _createClass as createClass, _extends as extends, _inheritsLoose as inheritsLoose };\n\nfunction _defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n}\n\nfunction _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n return Constructor;\n}\n\nfunction _extends() {\n _extends = Object.assign || function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n };\n\n return _extends.apply(this, arguments);\n}\n\nfunction _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n subClass.__proto__ = superClass;\n}","import $ from 'jquery'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): util.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Util = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Private TransitionEnd Helpers\n * ------------------------------------------------------------------------\n */\n\n let transition = false\n\n const MAX_UID = 1000000\n\n // Shoutout AngusCroll (https://goo.gl/pxwQGp)\n function toType(obj) {\n return {}.toString.call(obj).match(/\\s([a-zA-Z]+)/)[1].toLowerCase()\n }\n\n function getSpecialTransitionEndEvent() {\n return {\n bindType: transition.end,\n delegateType: transition.end,\n handle(event) {\n if ($(event.target).is(this)) {\n return event.handleObj.handler.apply(this, arguments) // eslint-disable-line prefer-rest-params\n }\n return undefined // eslint-disable-line no-undefined\n }\n }\n }\n\n function transitionEndTest() {\n if (typeof window !== 'undefined' && window.QUnit) {\n return false\n }\n\n return {\n end: 'transitionend'\n }\n }\n\n function transitionEndEmulator(duration) {\n let called = false\n\n $(this).one(Util.TRANSITION_END, () => {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n }\n\n function setTransitionEndSupport() {\n transition = transitionEndTest()\n\n $.fn.emulateTransitionEnd = transitionEndEmulator\n\n if (Util.supportsTransitionEnd()) {\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n }\n }\n\n function escapeId(selector) {\n // We escape IDs in case of special selectors (selector = '#myId:something')\n // $.escapeSelector does not exist in jQuery < 3\n selector = typeof $.escapeSelector === 'function' ? $.escapeSelector(selector).substr(1)\n : selector.replace(/(:|\\.|\\[|\\]|,|=|@)/g, '\\\\$1')\n\n return selector\n }\n\n /**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\n const Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n if (!selector || selector === '#') {\n selector = element.getAttribute('href') || ''\n }\n\n // If it's an ID\n if (selector.charAt(0) === '#') {\n selector = escapeId(selector)\n }\n\n try {\n const $selector = $(document).find(selector)\n return $selector.length > 0 ? selector : null\n } catch (err) {\n return null\n }\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(transition.end)\n },\n\n supportsTransitionEnd() {\n return Boolean(transition)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n }\n }\n\n setTransitionEndSupport()\n\n return Util\n})($)\n\nexport default Util\n","import $ from 'jquery'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Alert = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'alert'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.alert'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const TRANSITION_DURATION = 150\n\n const Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n }\n\n const Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n element = element || this._element\n\n const rootElement = this._getRootElement(element)\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = $(selector)[0]\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!Util.supportsTransitionEnd() ||\n !$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(TRANSITION_DURATION)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n )\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Alert._jQueryInterface\n $.fn[NAME].Constructor = Alert\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n }\n\n return Alert\n})($)\n\nexport default Alert\n","import $ from 'jquery'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Button = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'button'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.button'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n }\n\n const Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLE : '[data-toggle=\"buttons\"]',\n INPUT : 'input',\n ACTIVE : '.active',\n BUTTON : '.btn'\n }\n\n const Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLE\n )[0]\n\n if (rootElement) {\n const input = $(this._element).find(Selector.INPUT)[0]\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n $(this._element).hasClass(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = $(rootElement).find(Selector.ACTIVE)[0]\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n }\n\n if (triggerChangeEvent) {\n if (input.hasAttribute('disabled') ||\n rootElement.hasAttribute('disabled') ||\n input.classList.contains('disabled') ||\n rootElement.classList.contains('disabled')) {\n return\n }\n input.checked = !$(this._element).hasClass(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !$(this._element).hasClass(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n event.preventDefault()\n\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Button._jQueryInterface\n $.fn[NAME].Constructor = Button\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n }\n\n return Button\n})($)\n\nexport default Button\n","import $ from 'jquery'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Carousel = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'carousel'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.carousel'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const TRANSITION_DURATION = 600\n const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\n const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\n const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\n\n const Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true\n }\n\n const DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean'\n }\n\n const Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n }\n\n const Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item'\n }\n\n const Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n\n this._isPaused = false\n this._isSliding = false\n\n this.touchTimeout = null\n\n this._config = this._getConfig(config)\n this._element = $(element)[0]\n this._indicatorsElement = $(this._element).find(Selector.INDICATORS)[0]\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if ($(this._element).find(Selector.NEXT_PREV)[0] &&\n Util.supportsTransitionEnd()) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n if ('ontouchstart' in document.documentElement) {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n $(this._element).on(Event.TOUCHEND, () => {\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n })\n }\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = $.makeArray($(element).parent().find(Selector.ITEM))\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex($(this._element).find(Selector.ACTIVE_ITEM)[0])\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n $(this._indicatorsElement)\n .find(Selector.ACTIVE)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if (Util.supportsTransitionEnd() &&\n $(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(TRANSITION_DURATION)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n $(window).on(Event.LOAD_DATA_API, () => {\n $(Selector.DATA_RIDE).each(function () {\n const $carousel = $(this)\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n })\n })\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Carousel._jQueryInterface\n $.fn[NAME].Constructor = Carousel\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n }\n\n return Carousel\n})($)\n\nexport default Carousel\n","import $ from 'jquery'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Collapse = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'collapse'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.collapse'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const TRANSITION_DURATION = 600\n\n const Default = {\n toggle : true,\n parent : ''\n }\n\n const DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n }\n\n const Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n }\n\n const Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n }\n\n const Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = $.makeArray($(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n const tabToggles = $(Selector.DATA_TOGGLE)\n for (let i = 0; i < tabToggles.length; i++) {\n const elem = tabToggles[i]\n const selector = Util.getSelectorFromElement(elem)\n if (selector !== null && $(selector).filter(element).length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = $.makeArray(\n $(this._parent)\n .find(Selector.ACTIVES)\n .filter(`[data-parent=\"${this._config.parent}\"]`)\n )\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length > 0) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n if (!Util.supportsTransitionEnd()) {\n complete()\n return\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(TRANSITION_DURATION)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n if (this._triggerArray.length > 0) {\n for (let i = 0; i < this._triggerArray.length; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n if (selector !== null) {\n const $elem = $(selector)\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n\n if (!Util.supportsTransitionEnd()) {\n complete()\n return\n }\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(TRANSITION_DURATION)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent = null\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = $(this._config.parent)[0]\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n $(parent).find(selector).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n if (element) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length > 0) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? $(selector)[0] : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n $(selector).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n })\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Collapse._jQueryInterface\n $.fn[NAME].Constructor = Collapse\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n }\n\n return Collapse\n})($)\n\nexport default Collapse\n","import $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Dropdown = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'dropdown'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.dropdown'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\n const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\n const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\n const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\n const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\n const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\n const Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n }\n\n const Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled)'\n }\n\n const AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n }\n\n const Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent'\n }\n\n const DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this._element)\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap dropdown require Popper.js (https://popper.js.org)')\n }\n let element = this._element\n // For dropup with alignment we use the parent as popper container\n if ($(parent).hasClass(ClassName.DROPUP)) {\n if ($(this._menu).hasClass(ClassName.MENULEFT) || $(this._menu).hasClass(ClassName.MENURIGHT)) {\n element = parent\n }\n }\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(element, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $('body').children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n this._menu = $(parent).find(Selector.MENU)[0]\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element).parent()\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getPopperConfig() {\n const offsetConf = {}\n if (typeof this._config.offset === 'function') {\n offsetConf.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets) || {}\n }\n return data\n }\n } else {\n offsetConf.offset = this._config.offset\n }\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: offsetConf,\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n return popperConfig\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = $.makeArray($(Selector.DATA_TOGGLE))\n for (let i = 0; i < toggles.length; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $('body').children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = $(selector)[0]\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) ||\n isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = $(parent).find(Selector.DATA_TOGGLE)[0]\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = $(parent).find(Selector.VISIBLE_ITEMS).get()\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Dropdown._jQueryInterface\n $.fn[NAME].Constructor = Dropdown\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n }\n\n return Dropdown\n})($, Popper)\n\nexport default Dropdown\n","import $ from 'jquery'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Modal = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'modal'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.modal'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const TRANSITION_DURATION = 300\n const BACKDROP_TRANSITION_DURATION = 150\n const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\n const Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n }\n\n const DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n }\n\n const Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show'\n }\n\n const Selector = {\n DIALOG : '.modal-dialog',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top',\n NAVBAR_TOGGLER : '.navbar-toggler'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = $(element).find(Selector.DIALOG)[0]\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._originalBodyPadding = 0\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isTransitioning || this._isShown) {\n return\n }\n\n if (Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n $(document.body).addClass(ClassName.OPEN)\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (this._isTransitioning || !this._isShown) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n\n const transition = Util.supportsTransitionEnd() && $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n if (transition) {\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(TRANSITION_DURATION)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n $(window, document, this._element, this._backdrop).off(EVENT_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _showElement(relatedTarget) {\n const transition = Util.supportsTransitionEnd() &&\n $(this._element).hasClass(ClassName.FADE)\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.scrollTop = 0\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(TRANSITION_DURATION)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n event.preventDefault()\n this.hide()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n const doAnimate = Util.supportsTransitionEnd() && animate\n\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n $(this._backdrop).addClass(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n if (this._config.backdrop === 'static') {\n this._element.focus()\n } else {\n this.hide()\n }\n })\n\n if (doAnimate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!doAnimate) {\n callback()\n return\n }\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if (Util.supportsTransitionEnd() &&\n $(this._element).hasClass(ClassName.FADE)) {\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(BACKDROP_TRANSITION_DURATION)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n\n // Adjust fixed content padding\n $(Selector.FIXED_CONTENT).each((index, element) => {\n const actualPadding = $(element)[0].style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element).data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(Selector.STICKY_CONTENT).each((index, element) => {\n const actualMargin = $(element)[0].style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust navbar-toggler margin\n $(Selector.NAVBAR_TOGGLER).each((index, element) => {\n const actualMargin = $(element)[0].style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element).data('margin-right', actualMargin).css('margin-right', `${parseFloat(calculatedMargin) + this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $('body').css('padding-right')\n $('body').data('padding-right', actualPadding).css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n $(Selector.FIXED_CONTENT).each((index, element) => {\n const padding = $(element).data('padding-right')\n if (typeof padding !== 'undefined') {\n $(element).css('padding-right', padding).removeData('padding-right')\n }\n })\n\n // Restore sticky content and navbar-toggler margin\n $(`${Selector.STICKY_CONTENT}, ${Selector.NAVBAR_TOGGLER}`).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $('body').data('padding-right')\n if (typeof padding !== 'undefined') {\n $('body').css('padding-right', padding).removeData('padding-right')\n }\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Modal.Default,\n ...$(this).data(),\n ...typeof config === 'object' && config\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = $(selector)[0]\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n })\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Modal._jQueryInterface\n $.fn[NAME].Constructor = Modal\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n }\n\n return Modal\n})($)\n\nexport default Modal\n","import $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Tooltip = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'tooltip'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.tooltip'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const TRANSITION_DURATION = 150\n const CLASS_PREFIX = 'bs-tooltip'\n const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\n const DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)'\n }\n\n const AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n }\n\n const Default = {\n animation : true,\n template : '
' +\n '
' +\n '
',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent'\n }\n\n const HoverState = {\n SHOW : 'show',\n OUT : 'out'\n }\n\n const Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n }\n\n const ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n }\n\n const Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n }\n\n const Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n }\n\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Tooltip {\n constructor(element, config) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap tooltips require Popper.js (https://popper.js.org)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal')\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const isInTheDom = $.contains(\n this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this.config.container === false ? document.body : $(this.config.container)\n\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, {\n placement: attachment,\n modifiers: {\n offset: {\n offset: this.config.offset\n },\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => {\n this._handlePopperPlacementChange(data)\n }\n })\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $('body').children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if (Util.supportsTransitionEnd() && $(this.tip).hasClass(ClassName.FADE)) {\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(Tooltip._TRANSITION_DURATION)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $('body').children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if (Util.supportsTransitionEnd() &&\n $(this.tip).hasClass(ClassName.FADE)) {\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(TRANSITION_DURATION)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n this.setElementContent($tip.find(Selector.TOOLTIP_INNER), this.getTitle())\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n const html = this.config.html\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n } else {\n $element[html ? 'html' : 'text'](content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n () => this.hide()\n )\n })\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n if (this.element.getAttribute('title') ||\n titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) ||\n context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this.element).data(),\n ...config\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(data) {\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(data.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Tooltip._jQueryInterface\n $.fn[NAME].Constructor = Tooltip\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n }\n\n return Tooltip\n})($, Popper)\n\nexport default Tooltip\n","import $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst Popover = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'popover'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.popover'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const CLASS_PREFIX = 'bs-popover'\n const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\n const Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
' +\n '
' +\n '

' +\n '
'\n }\n\n const DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n }\n\n const ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n }\n\n const Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n }\n\n const Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /destroy|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n }\n\n /**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n $.fn[NAME] = Popover._jQueryInterface\n $.fn[NAME].Constructor = Popover\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n }\n\n return Popover\n})($)\n\nexport default Popover\n","import $ from 'jquery'\nimport Util from './util'\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.0.0): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst ScrollSpy = (($) => {\n /**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\n const NAME = 'scrollspy'\n const VERSION = '4.0.0'\n const DATA_KEY = 'bs.scrollspy'\n const EVENT_KEY = `.${DATA_KEY}`\n const DATA_API_KEY = '.data-api'\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Default = {\n offset : 10,\n method : 'auto',\n target : ''\n }\n\n const DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n }\n\n const Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n }\n\n const ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n }\n\n const Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n }\n\n const OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n }\n\n /**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\n class ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = $.makeArray($(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = $(targetSelector)[0]\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n for (let i = this._offsets.length; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n let queries = this._selector.split(',')\n // eslint-disable-next-line arrow-body-style\n queries = queries.map((selector) => {\n return `${selector}[data-target=\"${target}\"],` +\n `${selector}[href=\"${target}\"]`\n })\n\n const $link = $(queries.join(','))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both