diff --git a/.github/workflows/prevent-env-file.yml b/.github/workflows/prevent-env-file.yml new file mode 100644 index 0000000..a627e02 --- /dev/null +++ b/.github/workflows/prevent-env-file.yml @@ -0,0 +1,22 @@ +name: Prevent .env files in PRs + +on: + pull_request: + branches: + - main + +jobs: + check-env-files: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Search for .env files + run: | + if git diff --name-only origin/main...HEAD | grep -E '(^|/)\.env'; then + echo "❌ PR contain .env file, merge is forbiden." + exit 1 + else + echo "✅ There are no .env files in this PR-u." + fi diff --git a/.gitignore b/.gitignore index 5a73fbc..7c4902f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -# MacOS -.DS_Store +**/.DS_Store +**/.idea/** diff --git a/.vs/FitPlusPlus/FileContentIndex/2e89363d-bed5-4084-bbc4-9b3589b5bdad.vsidx b/.vs/FitPlusPlus/FileContentIndex/2e89363d-bed5-4084-bbc4-9b3589b5bdad.vsidx deleted file mode 100644 index 433cd05..0000000 Binary files a/.vs/FitPlusPlus/FileContentIndex/2e89363d-bed5-4084-bbc4-9b3589b5bdad.vsidx and /dev/null differ diff --git a/.vs/FitPlusPlus/v17/.wsuo b/.vs/FitPlusPlus/v17/.wsuo deleted file mode 100644 index 2e93c53..0000000 Binary files a/.vs/FitPlusPlus/v17/.wsuo and /dev/null differ diff --git a/.vs/FitPlusPlus/v17/DocumentLayout.json b/.vs/FitPlusPlus/v17/DocumentLayout.json deleted file mode 100644 index b498df4..0000000 --- a/.vs/FitPlusPlus/v17/DocumentLayout.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version": 1, - "WorkspaceRootPath": "D:\\FitPlusPlus\\", - "Documents": [], - "DocumentGroupContainers": [ - { - "Orientation": 0, - "VerticalTabListWidth": 256, - "DocumentGroups": [] - } - ] -} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json deleted file mode 100644 index 29908f2..0000000 --- a/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ExpandedNodes": [ - "", - "\\Fitness", - "\\Fitness\\Backend", - "\\Fitness\\Backend\\Services" - ], - "SelectedNode": "\\Fitness\\Backend\\Services\\ClientService", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index eef3636..0000000 Binary files a/.vs/slnx.sqlite and /dev/null differ diff --git a/Fitness/Backend/.config/dotnet-tools.json b/Fitness/Backend/.config/dotnet-tools.json new file mode 100644 index 0000000..4f48799 --- /dev/null +++ b/Fitness/Backend/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.0", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/.dockerignore b/Fitness/Backend/.dockerignore index fe1152b..38bece4 100644 --- a/Fitness/Backend/.dockerignore +++ b/Fitness/Backend/.dockerignore @@ -1,30 +1,25 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md -!**/.gitignore -!.git/HEAD -!.git/config -!.git/packed-refs -!.git/refs/heads/** \ No newline at end of file +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Fitness/Backend/.gitignore b/Fitness/Backend/.gitignore index d38028c..7bddc9b 100644 --- a/Fitness/Backend/.gitignore +++ b/Fitness/Backend/.gitignore @@ -2,6 +2,8 @@ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea + # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml @@ -56,6 +58,8 @@ out/ # mpeltonen/sbt-idea plugin .idea_modules/ +.idea/ + # JIRA plugin atlassian-ide-plugin.xml @@ -487,3 +491,4 @@ FodyWeavers.xsd # .env **/.env +uploads/** \ No newline at end of file diff --git a/Fitness/Backend/Common/EventBus.Messages/Constants/EventBusConstants.cs b/Fitness/Backend/Common/EventBus.Messages/Constants/EventBusConstants.cs index 79c49f0..1c9c503 100644 --- a/Fitness/Backend/Common/EventBus.Messages/Constants/EventBusConstants.cs +++ b/Fitness/Backend/Common/EventBus.Messages/Constants/EventBusConstants.cs @@ -8,8 +8,9 @@ namespace EventBus.Messages.Constants { public static class EventBusConstants { - public const string BookTrainingQueue = "bookingtraining-queue"; - public const string CancellingTrainingQueue = "cancellingtraining-queue"; public const string NotificationQueue = "notification-queue"; + public const string IndividualReservationQueue = "individual-reservation-queue"; + public const string GroupReservationQueue = "group-reservation-queue"; + public const string ReviewQueue = "review-queue"; } } diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/BookTrainingEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/BookTrainingEvent.cs deleted file mode 100644 index 93cb884..0000000 --- a/Fitness/Backend/Common/EventBus.Messages/Events/BookTrainingEvent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EventBus.Messages.Events -{ - public class BookTrainingEvent : IntegrationBaseEvent - { - public string ClientId { get; set; } - public string TrainerId { get; set; } - public string TrainingType { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - public bool IsBooking { get; set; } - } -} diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/GroupReservationEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/GroupReservationEvent.cs new file mode 100644 index 0000000..e81e597 --- /dev/null +++ b/Fitness/Backend/Common/EventBus.Messages/Events/GroupReservationEvent.cs @@ -0,0 +1,22 @@ +namespace EventBus.Messages.Events; + +public class GroupReservationEvent : IntegrationBaseEvent +{ + public string ReservationId { get; set; } + public string? ClientId { get; set; } + public string? TrainerId { get; set; } + public string? TrainingName { get; set; } + public int? Capacity { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public GroupReservationEventType EventType { get; set; } +} + +public enum GroupReservationEventType +{ + Added, + Removed, + ClientBooked, + ClientCancelled, +} \ No newline at end of file diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/IndividualReservationEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/IndividualReservationEvent.cs new file mode 100644 index 0000000..b0f5cec --- /dev/null +++ b/Fitness/Backend/Common/EventBus.Messages/Events/IndividualReservationEvent.cs @@ -0,0 +1,20 @@ +namespace EventBus.Messages.Events; + +public class IndividualReservationEvent : IntegrationBaseEvent +{ + public string ReservationId { get; set; } + public string? ClientId { get; set; } + public string? TrainerId { get; set; } + public string? TrainingTypeId { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public IndividualReservationEventType EventType { get; set; } +} + +public enum IndividualReservationEventType +{ + Booked, + CancelledByClient, + CancelledByTrainer, +} \ No newline at end of file diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/NotificationEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/NotificationEvent.cs index 3950429..8d842fe 100644 --- a/Fitness/Backend/Common/EventBus.Messages/Events/NotificationEvent.cs +++ b/Fitness/Backend/Common/EventBus.Messages/Events/NotificationEvent.cs @@ -2,18 +2,11 @@ namespace EventBus.Messages.Events; public class NotificationEvent : IntegrationBaseEvent { - public string UserId { get; set; } - public UserType UType { get; set; } + public IDictionary UserIdToUserType { get; set; } public string Title { get; set; } public string Content { get; set; } - public NotificationType NType { get; set; } + public NotificationType Type { get; set; } public bool Email { get; set; } - - public enum UserType - { - Client, - Trainer - } public enum NotificationType { diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/ReviewEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/ReviewEvent.cs new file mode 100644 index 0000000..91fd414 --- /dev/null +++ b/Fitness/Backend/Common/EventBus.Messages/Events/ReviewEvent.cs @@ -0,0 +1,16 @@ +namespace EventBus.Messages.Events; + +public class ReviewEvent : IntegrationBaseEvent +{ + public string ReservationId { get; set; } + public string? UserId { get; set; } + public string? Comment { get; set; } + public int? Rating { get; set; } + public ReviewEventType EventType { get; set; } +} + +public enum ReviewEventType +{ + ClientReview, + TrainerReview +} \ No newline at end of file diff --git a/Fitness/Backend/Common/EventBus.Messages/Events/TrainerCancellingTrainingEvent.cs b/Fitness/Backend/Common/EventBus.Messages/Events/TrainerCancellingTrainingEvent.cs deleted file mode 100644 index 033b354..0000000 --- a/Fitness/Backend/Common/EventBus.Messages/Events/TrainerCancellingTrainingEvent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EventBus.Messages.Events -{ - public class TrainerCancellingTrainingEvent - { - public string ClientId { get; set; } - public string TrainerId { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - } -} diff --git a/Fitness/Backend/Fitness.sln b/Fitness/Backend/Fitness.sln index f07503b..08e2b56 100644 --- a/Fitness/Backend/Fitness.sln +++ b/Fitness/Backend/Fitness.sln @@ -25,6 +25,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Messages", "Common EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaymentService.API", "Services\PaymentService\PaymentService.API\PaymentService.API.csproj", "{39B7B5E9-F9D8-4A1F-B4E5-216248755CCA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnalyticsService.Common", "Services\AnalyticsService\AnalyticsService.Common\AnalyticsService.Common.csproj", "{64737429-3650-42B7-A1ED-AB7573B51278}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnalyticsService.API", "Services\AnalyticsService\AnalyticsService.API\AnalyticsService.API.csproj", "{29B1A1CD-24C1-41D5-B8C8-AD1CE3AE180A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReservationService.API", "Services\ReservationService\ReservationService.API\ReservationService.API.csproj", "{F3C942A5-0892-4451-9092-900F63365EC1}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotificationService.API", "Services\NotificationService\NotificationService.API\NotificationService.API.csproj", "{42D48CA2-5675-47F2-BAE7-91E595258CBA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientService.Common", "Services\ClientService\ClientService.Common\ClientService.Common.csproj", "{FA536695-2D2D-4B71-8883-95B6EEF320F6}" @@ -35,12 +41,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerService.Common", "Se EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerService.GRPC", "Services\TrainerService\TrainerService.GRPC\TrainerService.GRPC.csproj", "{8A41AB5B-51F7-4F30-9B49-19BF8E09FC5B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatService.API", "Services\ChatService.API\ChatService.API.csproj", "{CE504321-77C1-4981-B815-489CA83FB163}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatService.API", "Services\ChatService\ChatService.API\ChatService.API.csproj", "{CE504321-77C1-4981-B815-489CA83FB163}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatewayService.API", "Services\GatewayService\GatewayService.API\GatewayService.API.csproj", "{0E6E9A4F-6271-45A4-BA0A-9468C6F15275}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsulConfig.Settings", "Common\ConsulConfig.Settings\ConsulConfig.Settings.csproj", "{E8032951-A2BF-4DA6-B433-C3004A2D1021}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{1D121431-9B0F-4592-9D0B-90B2E191AA64}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NutritionService", "NutritionService", "{F2366384-2A1A-4EFE-8CDC-284F923B0A84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NutritionService.API", "Services\NutritionService\NutritionService.API\NutritionService.API.csproj", "{7D625DE7-ABFB-4782-8F3F-5D9A701439D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "videoTrainingService.API", "Services\videoTrainingService\videoTrainingService.API\videoTrainingService.API.csproj", "{5A3A398E-4133-43EF-A92C-338E8E13715C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +97,10 @@ Global {39B7B5E9-F9D8-4A1F-B4E5-216248755CCA}.Debug|Any CPU.Build.0 = Debug|Any CPU {39B7B5E9-F9D8-4A1F-B4E5-216248755CCA}.Release|Any CPU.ActiveCfg = Release|Any CPU {39B7B5E9-F9D8-4A1F-B4E5-216248755CCA}.Release|Any CPU.Build.0 = Release|Any CPU + {F3C942A5-0892-4451-9092-900F63365EC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3C942A5-0892-4451-9092-900F63365EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3C942A5-0892-4451-9092-900F63365EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3C942A5-0892-4451-9092-900F63365EC1}.Release|Any CPU.Build.0 = Release|Any CPU {42D48CA2-5675-47F2-BAE7-91E595258CBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42D48CA2-5675-47F2-BAE7-91E595258CBA}.Debug|Any CPU.Build.0 = Debug|Any CPU {42D48CA2-5675-47F2-BAE7-91E595258CBA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -115,6 +133,22 @@ Global {E8032951-A2BF-4DA6-B433-C3004A2D1021}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8032951-A2BF-4DA6-B433-C3004A2D1021}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8032951-A2BF-4DA6-B433-C3004A2D1021}.Release|Any CPU.Build.0 = Release|Any CPU + {64737429-3650-42B7-A1ED-AB7573B51278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64737429-3650-42B7-A1ED-AB7573B51278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64737429-3650-42B7-A1ED-AB7573B51278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64737429-3650-42B7-A1ED-AB7573B51278}.Release|Any CPU.Build.0 = Release|Any CPU + {29B1A1CD-24C1-41D5-B8C8-AD1CE3AE180A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29B1A1CD-24C1-41D5-B8C8-AD1CE3AE180A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29B1A1CD-24C1-41D5-B8C8-AD1CE3AE180A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29B1A1CD-24C1-41D5-B8C8-AD1CE3AE180A}.Release|Any CPU.Build.0 = Release|Any CPU + {5A3A398E-4133-43EF-A92C-338E8E13715C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A3A398E-4133-43EF-A92C-338E8E13715C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A3A398E-4133-43EF-A92C-338E8E13715C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A3A398E-4133-43EF-A92C-338E8E13715C}.Release|Any CPU.Build.0 = Release|Any CPU + {7D625DE7-ABFB-4782-8F3F-5D9A701439D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D625DE7-ABFB-4782-8F3F-5D9A701439D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D625DE7-ABFB-4782-8F3F-5D9A701439D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D625DE7-ABFB-4782-8F3F-5D9A701439D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +157,8 @@ Global {DD1D07DE-12AA-437E-AC5C-B6F21A9D64C8} = {89F11E05-9275-456A-B680-9E47AEFC6A05} {4E7EFF7C-F700-473B-9265-48C289A497BF} = {08A30FB7-C7FE-49F3-B26F-FB0F9945EF7A} {E8032951-A2BF-4DA6-B433-C3004A2D1021} = {08A30FB7-C7FE-49F3-B26F-FB0F9945EF7A} + {F2366384-2A1A-4EFE-8CDC-284F923B0A84} = {1D121431-9B0F-4592-9D0B-90B2E191AA64} + {7D625DE7-ABFB-4782-8F3F-5D9A701439D1} = {F2366384-2A1A-4EFE-8CDC-284F923B0A84} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B317DCB6-F066-4D68-9E03-E9DF67260B22} diff --git a/Fitness/Backend/Security/IdentityServer/IdentityServer.csproj b/Fitness/Backend/Security/IdentityServer/IdentityServer.csproj index 1d37a83..3a2f5e7 100644 --- a/Fitness/Backend/Security/IdentityServer/IdentityServer.csproj +++ b/Fitness/Backend/Security/IdentityServer/IdentityServer.csproj @@ -34,4 +34,8 @@ + + + + diff --git a/Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.Designer.cs b/Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.Designer.cs similarity index 97% rename from Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.Designer.cs rename to Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.Designer.cs index bf6c3f6..03df519 100644 --- a/Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.Designer.cs +++ b/Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace IdentityServer.Migrations { [DbContext(typeof(ApplicationContext))] - [Migration("20241123074920_Initial")] + [Migration("20250929092457_Initial")] partial class Initial { /// @@ -150,19 +150,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "ddcce0a9-75cc-4e5b-9b92-04e6ebb8432e", + Id = "397573a6-fde1-4c71-a7ee-c3a475528ffb", Name = "Admin", NormalizedName = "ADMIN" }, new { - Id = "08a19f84-14c0-48c5-9cba-f13b9bd4bed2", + Id = "a0c2a8b8-feaa-44f7-8b93-c391497d0ef9", Name = "Trainer", NormalizedName = "TRAINER" }, new { - Id = "4ecf06ad-a066-4e44-a56d-e41b24e3f2cf", + Id = "f9ad2c7c-4042-46c3-a039-b6c05a0b9c5a", Name = "Client", NormalizedName = "CLIENT" }); diff --git a/Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.cs b/Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.cs similarity index 98% rename from Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.cs rename to Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.cs index 7a95923..a59fddd 100644 --- a/Fitness/Backend/Security/IdentityServer/Migrations/20241123074920_Initial.cs +++ b/Fitness/Backend/Security/IdentityServer/Migrations/20250929092457_Initial.cs @@ -184,9 +184,9 @@ protected override void Up(MigrationBuilder migrationBuilder) columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, values: new object[,] { - { "08a19f84-14c0-48c5-9cba-f13b9bd4bed2", null, "Trainer", "TRAINER" }, - { "4ecf06ad-a066-4e44-a56d-e41b24e3f2cf", null, "Client", "CLIENT" }, - { "ddcce0a9-75cc-4e5b-9b92-04e6ebb8432e", null, "Admin", "ADMIN" } + { "397573a6-fde1-4c71-a7ee-c3a475528ffb", null, "Admin", "ADMIN" }, + { "a0c2a8b8-feaa-44f7-8b93-c391497d0ef9", null, "Trainer", "TRAINER" }, + { "f9ad2c7c-4042-46c3-a039-b6c05a0b9c5a", null, "Client", "CLIENT" } }); migrationBuilder.CreateIndex( diff --git a/Fitness/Backend/Security/IdentityServer/Migrations/ApplicationContextModelSnapshot.cs b/Fitness/Backend/Security/IdentityServer/Migrations/ApplicationContextModelSnapshot.cs index 8047aac..2d90db9 100644 --- a/Fitness/Backend/Security/IdentityServer/Migrations/ApplicationContextModelSnapshot.cs +++ b/Fitness/Backend/Security/IdentityServer/Migrations/ApplicationContextModelSnapshot.cs @@ -42,7 +42,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserId"); - b.ToTable("RefreshTokens"); + b.ToTable("RefreshTokens", (string)null); }); modelBuilder.Entity("IdentityServer.Entities.User", b => @@ -147,19 +147,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasData( new { - Id = "ddcce0a9-75cc-4e5b-9b92-04e6ebb8432e", + Id = "397573a6-fde1-4c71-a7ee-c3a475528ffb", Name = "Admin", NormalizedName = "ADMIN" }, new { - Id = "08a19f84-14c0-48c5-9cba-f13b9bd4bed2", + Id = "a0c2a8b8-feaa-44f7-8b93-c391497d0ef9", Name = "Trainer", NormalizedName = "TRAINER" }, new { - Id = "4ecf06ad-a066-4e44-a56d-e41b24e3f2cf", + Id = "f9ad2c7c-4042-46c3-a039-b6c05a0b9c5a", Name = "Client", NormalizedName = "CLIENT" }); diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.csproj b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.csproj new file mode 100644 index 0000000..007f4e0 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + + + + + + + + + + + + .dockerignore + + + + diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.http b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.http new file mode 100644 index 0000000..5c8b049 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.http @@ -0,0 +1,6 @@ +@AnalyticsService.API_HostAddress = http://localhost:5250 + +GET {{AnalyticsService.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Controllers/AnalyticsController.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Controllers/AnalyticsController.cs new file mode 100644 index 0000000..c8a50b5 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Controllers/AnalyticsController.cs @@ -0,0 +1,58 @@ +using AnalyticsService.Common.Entities; +using AnalyticsService.Common.Repositories; +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace AnalyticsService.API.Controllers; + +[Authorize] +[ApiController] +[Route("api/v1/[controller]")] +public class AnalyticsController : ControllerBase +{ + private readonly IAnalyticsRepository _repository; + + public AnalyticsController(IAnalyticsRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + // Individual Trainings + [Authorize(Roles = "Admin, Trainer")] + [HttpGet("individual/trainer/{trainerId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetIndividualTrainingsByTrainerId(string trainerId) + { + var reservations = await _repository.GetIndividualTrainingsByTrainerId(trainerId); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client")] + [HttpGet("individual/client/{clientId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetIndividualTrainingsByClientId(string clientId) + { + var reservations = await _repository.GetIndividualTrainingsByClientId(clientId); + return Ok(reservations); + } + + // Group Trainings + [Authorize(Roles = "Admin, Trainer")] + [HttpGet("group/trainer/{trainerId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetGroupTrainingsByTrainerId(string trainerId) + { + var reservations = await _repository.GetGroupTrainingsByTrainerId(trainerId); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client")] + [HttpGet("group/client/{clientId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetGroupTrainingsByClientId(string clientId) + { + var reservations = await _repository.GetGroupTrainingsByClientId(clientId); + return Ok(reservations); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Dockerfile b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Dockerfile new file mode 100644 index 0000000..fe59099 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.csproj", "Services/AnalyticsService/AnalyticsService.API/"] +COPY ["Services/AnalyticsService/AnalyticsService.Common/AnalyticsService.Common.csproj", "Services/AnalyticsService/AnalyticsService.Common/"] +COPY ["Common/EventBus.Messages/EventBus.Messages.csproj", "Common/EventBus.Messages/"] +RUN dotnet restore "Services/AnalyticsService/AnalyticsService.API/AnalyticsService.API.csproj" +COPY . . +WORKDIR "/src/Services/AnalyticsService/AnalyticsService.API" +RUN dotnet build "AnalyticsService.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "AnalyticsService.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "AnalyticsService.API.dll"] diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/GroupReservationConsumer.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/GroupReservationConsumer.cs new file mode 100644 index 0000000..62bbb4d --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/GroupReservationConsumer.cs @@ -0,0 +1,74 @@ +using System.Text.Json; +using AnalyticsService.Common.Entities; +using AnalyticsService.Common.Repositories; +using EventBus.Messages.Events; +using MassTransit; +using GroupReservationEventType = EventBus.Messages.Events.GroupReservationEventType; + +namespace AnalyticsService.API.EventBusConsumers; + +public class GroupReservationConsumer : IConsumer +{ + private readonly IAnalyticsRepository _repository; + + public GroupReservationConsumer(IAnalyticsRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task Consume(ConsumeContext context) + { + GroupReservationEvent groupReservation = context.Message; + switch (groupReservation.EventType) + { + case GroupReservationEventType.Added: + { + GroupTraining groupTraining = new GroupTraining + { + ReservationId = groupReservation.ReservationId, + TrainerId = groupReservation.TrainerId, + ClientIds = [], + Capacity = groupReservation.Capacity, + StartTime = groupReservation.StartTime, + EndTime = groupReservation.EndTime, + Date = groupReservation.Date, + TrainerReview = null, + ClientReviews = null, + Status = GroupTrainingStatus.Active + }; + await _repository.CreateGroupTraining(groupTraining); + break; + } + case GroupReservationEventType.Removed: + { + GroupTraining groupTraining = + await _repository.GetGroupTrainingByReservationId(groupReservation.ReservationId); + groupTraining.Status = GroupTrainingStatus.Removed; + await _repository.UpdateGroupTraining(groupTraining); + break; + } + case GroupReservationEventType.ClientBooked: + { + GroupTraining groupTraining = + await _repository.GetGroupTrainingByReservationId(groupReservation.ReservationId); + if (groupReservation.ClientId != null) + { + groupTraining.ClientIds.Add(groupReservation.ClientId); + await _repository.UpdateGroupTraining(groupTraining); + } + break; + } + case GroupReservationEventType.ClientCancelled: + { + GroupTraining groupTraining = + await _repository.GetGroupTrainingByReservationId(groupReservation.ReservationId); + if (groupReservation.ClientId != null) + { + groupTraining.ClientIds.Remove(groupReservation.ClientId); + await _repository.UpdateGroupTraining(groupTraining); + } + break; + } + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/IndividualReservationConsumer.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/IndividualReservationConsumer.cs new file mode 100644 index 0000000..f4383f3 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/IndividualReservationConsumer.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using AnalyticsService.Common.Entities; +using AnalyticsService.Common.Repositories; +using AutoMapper; +using EventBus.Messages.Events; +using MassTransit; + +namespace AnalyticsService.API.EventBusConsumers; + +public class IndividualReservationConsumer : IConsumer +{ + private readonly IAnalyticsRepository _repository; + + public IndividualReservationConsumer(IAnalyticsRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + public async Task Consume(ConsumeContext context) + { + IndividualReservationEvent individualReservation = context.Message; + switch (individualReservation.EventType) + { + case IndividualReservationEventType.Booked: + { + IndividualTraining individualTraining = new IndividualTraining + { + ReservationId = individualReservation.ReservationId, + TrainerId = individualReservation.TrainerId, + ClientId = individualReservation.ClientId, + TrainingTypeId = individualReservation.TrainingTypeId, + StartTime = individualReservation.StartTime, + EndTime = individualReservation.EndTime, + Date = individualReservation.Date, + TrainerReview = null, + ClientReview = null, + Status = IndividualTrainingStatus.Active + }; + await _repository.CreateIndividualTraining(individualTraining); + break; + } + case IndividualReservationEventType.CancelledByClient: + { + IndividualTraining individualTraining = + await _repository.GetIndividualTrainingByReservationId(individualReservation.ReservationId); + individualTraining.Status = IndividualTrainingStatus.ClientCancelled; + await _repository.UpdateIndividualTraining(individualTraining); + break; + } + case IndividualReservationEventType.CancelledByTrainer: + { + IndividualTraining individualTraining = + await _repository.GetIndividualTrainingByReservationId(individualReservation.ReservationId); + individualTraining.Status = IndividualTrainingStatus.TrainerCancelled; + await _repository.UpdateIndividualTraining(individualTraining); + break; + } + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/ReviewConsumer.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/ReviewConsumer.cs new file mode 100644 index 0000000..9cc179b --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/EventBusConsumers/ReviewConsumer.cs @@ -0,0 +1,62 @@ +using System.Text.Json; +using AnalyticsService.Common.Entities; +using AnalyticsService.Common.Repositories; +using EventBus.Messages.Events; +using MassTransit; + +namespace AnalyticsService.API.EventBusConsumers; + +public class ReviewConsumer : IConsumer +{ + private readonly IAnalyticsRepository _repository; + + public ReviewConsumer(IAnalyticsRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task Consume(ConsumeContext context) + { + ReviewEvent review = context.Message; + Review analyticsReview = new Review + { + UserId = review.UserId, + Rating = review.Rating, + Comment = review.Comment + }; + var individualTraining = await _repository.GetIndividualTrainingByReservationId(review.ReservationId); + var groupTraining = await _repository.GetGroupTrainingByReservationId(review.ReservationId); + switch (review.EventType) + { + case ReviewEventType.ClientReview: + { + if (individualTraining != null) + { + individualTraining.ClientReview = analyticsReview; + await _repository.UpdateIndividualTraining(individualTraining); + } + else if (groupTraining != null) + { + groupTraining.ClientReviews ??= []; + groupTraining.ClientReviews.Add(analyticsReview); + await _repository.UpdateGroupTraining(groupTraining); + } + break; + } + case ReviewEventType.TrainerReview: + { + if (individualTraining != null) + { + individualTraining.TrainerReview = analyticsReview; + await _repository.UpdateIndividualTraining(individualTraining); + } + else if (groupTraining != null) + { + groupTraining.TrainerReview = analyticsReview; + await _repository.UpdateGroupTraining(groupTraining); + } + break; + } + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Program.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Program.cs new file mode 100644 index 0000000..38de1a6 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Program.cs @@ -0,0 +1,125 @@ +using System.Reflection; +using System.Text; +using AnalyticsService.API.EventBusConsumers; +using AnalyticsService.Common.Extensions; +using Consul; +using ConsulConfig.Settings; +using EventBus.Messages.Constants; +using MassTransit; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); +var consulConfig = builder.Configuration.GetSection("ConsulConfig").Get()!; +builder.Services.AddSingleton(consulConfig); +builder.Services.AddSingleton(provider => new ConsulClient(config => +{ + config.Address = new Uri(consulConfig.Address); +})); + + +builder.Services.AddControllers(); +builder.Services.AddAnalyticsCommonExtensions(); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// cors +builder.Services.AddCors(options => +{ + options.AddPolicy("CorsPolicy", builder => + builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); +}); + +//AutoMapper +builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); + +//EventBus +builder.Services.AddMassTransit(config => +{ + config.AddConsumer(); + config.AddConsumer(); + config.AddConsumer(); + config.UsingRabbitMq((ctx, cfg) => + { + cfg.Host(builder.Configuration["EventBusSettings:HostAddress"]); + cfg.ReceiveEndpoint(EventBusConstants.IndividualReservationQueue, c => + { + c.ConfigureConsumer(ctx); + }); + cfg.ReceiveEndpoint(EventBusConstants.GroupReservationQueue, c => + { + c.ConfigureConsumer(ctx); + }); + cfg.ReceiveEndpoint(EventBusConstants.ReviewQueue, c => + { + c.ConfigureConsumer(ctx); + }); + }); +}); + +var jwtSettings = builder.Configuration.GetSection("JwtSettings"); +var secretKey = jwtSettings.GetValue("secretKey"); + +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) + }; + }); + +var app = builder.Build(); + +app.Lifetime.ApplicationStarted.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + + var registration = new AgentServiceRegistration + { + ID = consulConfig.ServiceId, + Name = consulConfig.ServiceName, + Address = consulConfig.ServiceAddress, + Port = consulConfig.ServicePort + }; + + consulClient.Agent.ServiceRegister(registration).Wait(); +}); + +app.Lifetime.ApplicationStopping.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); +}); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("CorsPolicy"); + +app.UseRouting(); +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.Run(); diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Properties/launchSettings.json b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Properties/launchSettings.json new file mode 100644 index 0000000..2811bec --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33132", + "sslPort": 44358 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5250", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7193;http://localhost:5250", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/appsettings.Development.json b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/appsettings.Development.json new file mode 100644 index 0000000..808cf3c --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/appsettings.Development.json @@ -0,0 +1,27 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DatabaseSettings": { + "ConnectionString": "mongodb://localhost:27017" + }, + "JwtSettings": { + "validIssuer": "Fitness Identity", + "validAudience": "Fitness", + "secretKey": "MyVeryVerySecretMessageForSecretKey", + "expires": 15 + }, + "EventBusSettings": { + "HostAddress": "amqp://guest:guest@localhost:5672" + }, + "ConsulConfig": { + "Address": "http://consul:8500", + "ServiceName": "AnalyticsService.API", + "ServiceId": "AnalyticsService.API-1", + "ServiceAddress": "analyticsservice.api", + "ServicePort": 8080 + } +} diff --git a/Fitness/Backend/Services/ChatService.API/appsettings.json b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/appsettings.json similarity index 100% rename from Fitness/Backend/Services/ChatService.API/appsettings.json rename to Fitness/Backend/Services/AnalyticsService/AnalyticsService.API/appsettings.json diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/AnalyticsService.Common.csproj b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/AnalyticsService.Common.csproj new file mode 100644 index 0000000..9be7733 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/AnalyticsService.Common.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/AnalyticsContext.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/AnalyticsContext.cs new file mode 100644 index 0000000..ed08697 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/AnalyticsContext.cs @@ -0,0 +1,19 @@ +using AnalyticsService.Common.Entities; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace AnalyticsService.Common.Data; + +public class AnalyticsContext : IAnalyticsContext +{ + public AnalyticsContext(IConfiguration configuration) + { + var mongoClient = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString")); + var database = mongoClient.GetDatabase("AnalyticsDB"); + IndividualTrainings = database.GetCollection("IndividualTrainings"); + GroupTrainings = database.GetCollection("GroupTrainings"); + } + + public IMongoCollection IndividualTrainings { get; set; } + public IMongoCollection GroupTrainings { get; set; } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/IAnalyticsContext.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/IAnalyticsContext.cs new file mode 100644 index 0000000..256035a --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Data/IAnalyticsContext.cs @@ -0,0 +1,10 @@ +using AnalyticsService.Common.Entities; +using MongoDB.Driver; + +namespace AnalyticsService.Common.Data; + +public interface IAnalyticsContext +{ + IMongoCollection IndividualTrainings { get; set; } + IMongoCollection GroupTrainings { get; set; } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/GroupTraining.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/GroupTraining.cs new file mode 100644 index 0000000..fb490d0 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/GroupTraining.cs @@ -0,0 +1,27 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace AnalyticsService.Common.Entities; + +public class GroupTraining +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string ReservationId { get; set; } + public string? TrainerId { get; set; } + public List ClientIds { get; set; } + public int? Capacity { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public Review? TrainerReview { get; set; } + public List? ClientReviews { get; set; } + public GroupTrainingStatus Status { get; set; } = GroupTrainingStatus.Active; +} + +public enum GroupTrainingStatus +{ + Active, + Removed +} diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/IndividualTraining.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/IndividualTraining.cs new file mode 100644 index 0000000..865fb05 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/IndividualTraining.cs @@ -0,0 +1,28 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace AnalyticsService.Common.Entities; + +public class IndividualTraining +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string? ReservationId { get; set; } + public string? TrainerId { get; set; } + public string? ClientId { get; set; } + public string? TrainingTypeId { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public Review? TrainerReview { get; set; } + public Review? ClientReview { get; set; } + public IndividualTrainingStatus Status { get; set; } = IndividualTrainingStatus.Active; +} + +public enum IndividualTrainingStatus +{ + Active, + TrainerCancelled, + ClientCancelled +} diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/Review.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/Review.cs new file mode 100644 index 0000000..7e3496d --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Entities/Review.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace AnalyticsService.Common.Entities; + +public class Review +{ + public string? UserId { get; set; } + public int? Rating { get; set; } + public string? Comment { get; set; } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Extensions/AnalyticsCommonExtensions.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Extensions/AnalyticsCommonExtensions.cs new file mode 100644 index 0000000..fa0dd56 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Extensions/AnalyticsCommonExtensions.cs @@ -0,0 +1,15 @@ +using AnalyticsService.Common.Data; +using AnalyticsService.Common.Entities; +using AnalyticsService.Common.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace AnalyticsService.Common.Extensions; + +public static class AnalyticsCommonExtensions +{ + public static void AddAnalyticsCommonExtensions(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/AnalyticsRepository.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/AnalyticsRepository.cs new file mode 100644 index 0000000..795d5b2 --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/AnalyticsRepository.cs @@ -0,0 +1,85 @@ +using AnalyticsService.Common.Data; +using AnalyticsService.Common.Entities; +using AutoMapper; +using MongoDB.Driver; +using ZstdSharp.Unsafe; + +namespace AnalyticsService.Common.Repositories; + +public class AnalyticsRepository : IAnalyticsRepository +{ + private readonly IAnalyticsContext _context; + private readonly IMapper _mapper; + + // Individual Trainings + public AnalyticsRepository(IAnalyticsContext context, IMapper mapper) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public async Task CreateIndividualTraining(IndividualTraining individualTraining) + { + await _context.IndividualTrainings.InsertOneAsync(individualTraining); + } + + public async Task GetIndividualTrainingByReservationId(string reservationId) + { + return await _context.IndividualTrainings.Find(it => it.ReservationId == reservationId).FirstOrDefaultAsync(); + } + + public async Task> GetIndividualTrainingsByTrainerId(string trainerId) + { + return await _context.IndividualTrainings.Find(it => it.TrainerId == trainerId).ToListAsync(); + } + + public async Task> GetIndividualTrainingsByClientId(string clientId) + { + return await _context.IndividualTrainings.Find(it => it.ClientId == clientId).ToListAsync(); + } + + public async Task UpdateIndividualTraining(IndividualTraining individualTraining) + { + var result = await _context.IndividualTrainings.ReplaceOneAsync(it => it.Id == individualTraining.Id, individualTraining); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task DeleteIndividualTraining(string id) + { + var result = await _context.IndividualTrainings.DeleteOneAsync(it => it.Id == id); + return result.IsAcknowledged && result.DeletedCount > 0; + } + + public async Task CreateGroupTraining(GroupTraining groupTraining) + { + await _context.GroupTrainings.InsertOneAsync(groupTraining); + } + + // Group Trainings + public async Task> GetGroupTrainingsByTrainerId(string trainerId) + { + return await _context.GroupTrainings.Find(gt => gt.TrainerId == trainerId).ToListAsync(); + } + + public async Task GetGroupTrainingByReservationId(string reservationId) + { + return await _context.GroupTrainings.Find(gt => gt.ReservationId == reservationId).FirstOrDefaultAsync(); + } + + public async Task> GetGroupTrainingsByClientId(string clientId) + { + return await _context.GroupTrainings.Find(gt => gt.ClientIds.Contains(clientId)).ToListAsync(); + } + + public async Task UpdateGroupTraining(GroupTraining groupTraining) + { + var result = await _context.GroupTrainings.ReplaceOneAsync(gt => gt.Id == groupTraining.Id, groupTraining); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task DeleteGroupTraining(string id) + { + var result = await _context.GroupTrainings.DeleteOneAsync(gt => gt.Id == id); + return result.IsAcknowledged && result.DeletedCount > 0; + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/IAnalyticsRepository.cs b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/IAnalyticsRepository.cs new file mode 100644 index 0000000..3ead86c --- /dev/null +++ b/Fitness/Backend/Services/AnalyticsService/AnalyticsService.Common/Repositories/IAnalyticsRepository.cs @@ -0,0 +1,22 @@ +using AnalyticsService.Common.Entities; + +namespace AnalyticsService.Common.Repositories; + +public interface IAnalyticsRepository +{ + // Individual Trainings + Task CreateIndividualTraining(IndividualTraining individualTraining); + Task GetIndividualTrainingByReservationId(string reservationId); + Task> GetIndividualTrainingsByTrainerId(string trainerId); + Task> GetIndividualTrainingsByClientId(string clientId); + Task UpdateIndividualTraining(IndividualTraining individualTraining); + Task DeleteIndividualTraining(string id); + + // Group Trainings + Task CreateGroupTraining(GroupTraining groupTraining); + Task GetGroupTrainingByReservationId(string reservationId); + Task> GetGroupTrainingsByTrainerId(string trainerId); + Task> GetGroupTrainingsByClientId(string clientId); + Task UpdateGroupTraining(GroupTraining groupTraining); + Task DeleteGroupTraining(string id); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ChatService.API/ChatService.API.csproj b/Fitness/Backend/Services/ChatService/ChatService.API/ChatService.API.csproj similarity index 81% rename from Fitness/Backend/Services/ChatService.API/ChatService.API.csproj rename to Fitness/Backend/Services/ChatService/ChatService.API/ChatService.API.csproj index 9780ed4..5f08d46 100644 --- a/Fitness/Backend/Services/ChatService.API/ChatService.API.csproj +++ b/Fitness/Backend/Services/ChatService/ChatService.API/ChatService.API.csproj @@ -13,6 +13,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,14 +26,14 @@ - + .dockerignore - - + + diff --git a/Fitness/Backend/Services/ChatService.API/ChatService.API.http b/Fitness/Backend/Services/ChatService/ChatService.API/ChatService.API.http similarity index 100% rename from Fitness/Backend/Services/ChatService.API/ChatService.API.http rename to Fitness/Backend/Services/ChatService/ChatService.API/ChatService.API.http diff --git a/Fitness/Backend/Services/ChatService.API/Controllers/ChatController.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Controllers/ChatController.cs similarity index 90% rename from Fitness/Backend/Services/ChatService.API/Controllers/ChatController.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Controllers/ChatController.cs index 7ab8587..d4c3a8f 100644 --- a/Fitness/Backend/Services/ChatService.API/Controllers/ChatController.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Controllers/ChatController.cs @@ -1,13 +1,13 @@ using ChatService.API.Models; using ChatService.API.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace ChatService.API.Controllers; -//TO DO AUTHORIZATION AND AUTHENTICATION - +[Authorize] [ApiController] -[Route("api/[controller]")] +[Route("api/v1/[controller]")] public class ChatController : ControllerBase { private readonly IChatService _chatService; @@ -17,7 +17,7 @@ public ChatController(IChatService chatService) _chatService = chatService; } - + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet("sessions/{userId}/my-sessions-summary")] public async Task GetBasicInfoForSessions(string userId) { @@ -30,7 +30,7 @@ public async Task GetBasicInfoForSessions(string userId) return Ok(basicInfo); } - + [Authorize(Roles = "Trainer, Client")] [HttpPost("sessions/messages")] public async Task AddMessageToSession([FromQuery] string trainerId, [FromQuery] string clientId, [FromBody] string content, [FromQuery] string senderType) { @@ -45,6 +45,7 @@ public async Task AddMessageToSession([FromQuery] string trainerI } } + [Authorize(Roles = "Trainer, Client")] [HttpGet("sessions/messages")] public async Task GetMessagesFromSession([FromQuery] string trainerId, [FromQuery] string clientId) { @@ -65,7 +66,7 @@ public async Task GetMessagesFromSession([FromQuery] string train return Ok(messages); } - + [Authorize(Roles = "Client")] [HttpPost("sessions")] public async Task CreateChatSession([FromQuery] string trainerId, [FromQuery] string clientId) { @@ -80,6 +81,7 @@ public async Task CreateChatSession([FromQuery] string trainerId, } } + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet("sessions")] public async Task GetChatSession([FromQuery] string trainerId, [FromQuery] string clientId) { @@ -87,6 +89,7 @@ public async Task GetChatSession([FromQuery] string trainerId, [F return session != null ? Ok(session) : NotFound(new { Message = "Chat session not found." }); } + [Authorize(Roles = "Admin")] [HttpDelete("sessions")] public async Task DeleteChatSession([FromQuery] string trainerId, [FromQuery] string clientId) { @@ -95,6 +98,7 @@ public async Task DeleteChatSession([FromQuery] string trainerId, : NotFound(new { Message = "Session not found or already deleted." }); } + [Authorize(Roles = "Client")] [HttpPost("sessions/extend")] public async Task ExtendChatSession([FromQuery] string trainerId, [FromQuery] string clientId) { diff --git a/Fitness/Backend/Services/ChatService.API/Data/Context.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Data/Context.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Data/Context.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Data/Context.cs diff --git a/Fitness/Backend/Services/ChatService.API/Data/IContext.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Data/IContext.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Data/IContext.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Data/IContext.cs diff --git a/Fitness/Backend/Services/ChatService.API/Dockerfile b/Fitness/Backend/Services/ChatService/ChatService.API/Dockerfile similarity index 69% rename from Fitness/Backend/Services/ChatService.API/Dockerfile rename to Fitness/Backend/Services/ChatService/ChatService.API/Dockerfile index 62f5b7b..09f0254 100644 --- a/Fitness/Backend/Services/ChatService.API/Dockerfile +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Dockerfile @@ -7,10 +7,10 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY ["Services/ChatService.API/ChatService.API.csproj", "Services/ChatService.API/"] -RUN dotnet restore "Services/ChatService.API/ChatService.API.csproj" +COPY ["Services/ChatService/ChatService.API/ChatService.API.csproj", "Services/ChatService/ChatService.API/"] +RUN dotnet restore "Services/ChatService/ChatService.API/ChatService.API.csproj" COPY . . -WORKDIR "/src/Services/ChatService.API" +WORKDIR "/src/Services/ChatService/ChatService.API" RUN dotnet build "ChatService.API.csproj" -c $BUILD_CONFIGURATION -o /app/build FROM build AS publish diff --git a/Fitness/Backend/Services/ChatService.API/Middleware/WebSocketMiddleware.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Middleware/WebSocketMiddleware.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Middleware/WebSocketMiddleware.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Middleware/WebSocketMiddleware.cs diff --git a/Fitness/Backend/Services/ChatService.API/Models/ChatSession.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Models/ChatSession.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Models/ChatSession.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Models/ChatSession.cs diff --git a/Fitness/Backend/Services/ChatService.API/Models/Message.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Models/Message.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Models/Message.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Models/Message.cs diff --git a/Fitness/Backend/Services/ChatService.API/Models/MongoDBSettings.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Models/MongoDBSettings.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Models/MongoDBSettings.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Models/MongoDBSettings.cs diff --git a/Fitness/Backend/Services/ChatService.API/Models/Notification.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Models/Notification.cs similarity index 57% rename from Fitness/Backend/Services/ChatService.API/Models/Notification.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Models/Notification.cs index 81391ca..2c842c2 100644 --- a/Fitness/Backend/Services/ChatService.API/Models/Notification.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Models/Notification.cs @@ -2,18 +2,11 @@ namespace ChatService.API.Models; public class Notification { - public string UserId { get; set; } - public UserType UType { get; set; } + public IDictionary UserIdToUserType; public string Title { get; set; } public string Content { get; set; } - public NotificationType NType { get; set; } + public NotificationType Type { get; set; } public bool Email { get; set; } - - public enum UserType - { - Client, - Trainer - } public enum NotificationType { diff --git a/Fitness/Backend/Services/ChatService.API/Program.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Program.cs similarity index 74% rename from Fitness/Backend/Services/ChatService.API/Program.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Program.cs index a44e4b7..162d4a2 100644 --- a/Fitness/Backend/Services/ChatService.API/Program.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Program.cs @@ -1,3 +1,4 @@ +using System.Text; using ChatService.API.Data; using ChatService.API.Middleware; using ChatService.API.Models; @@ -10,7 +11,8 @@ using ConsulConfig.Settings; using EventBus.Messages.Events; using MassTransit; - +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -71,10 +73,33 @@ }); }); +var jwtSettings = builder.Configuration.GetSection("JwtSettings"); +var secretKey = jwtSettings.GetValue("secretKey"); +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) + }; + }); var app = builder.Build(); +app.UseCors("AllowAll"); + app.Lifetime.ApplicationStarted.Register(() => { var consulClient = app.Services.GetRequiredService(); @@ -96,8 +121,6 @@ consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); }); - - // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -106,11 +129,14 @@ } app.UseRouting(); -app.UseCors("AllowAll"); -app.UseHttpsRedirection(); + +app.UseAuthentication(); app.UseAuthorization(); + +// app.UseHttpsRedirection(); app.UseWebSockets(); app.UseMiddleware(); + app.MapControllers(); app.Run(); diff --git a/Fitness/Backend/Services/ChatService.API/Properties/launchSettings.json b/Fitness/Backend/Services/ChatService/ChatService.API/Properties/launchSettings.json similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Properties/launchSettings.json rename to Fitness/Backend/Services/ChatService/ChatService.API/Properties/launchSettings.json diff --git a/Fitness/Backend/Services/ChatService.API/Publishers/INotificationPublisher.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Publishers/INotificationPublisher.cs similarity index 72% rename from Fitness/Backend/Services/ChatService.API/Publishers/INotificationPublisher.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Publishers/INotificationPublisher.cs index 6798a3c..3eaaf33 100644 --- a/Fitness/Backend/Services/ChatService.API/Publishers/INotificationPublisher.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Publishers/INotificationPublisher.cs @@ -2,5 +2,5 @@ namespace ChatService.API.Publishers; public interface INotificationPublisher { - Task PublishNotification(string title, string content, string type, bool email, string userId, string userType); + Task PublishNotification(string title, string content, string type, bool email, IDictionary users); } \ No newline at end of file diff --git a/Fitness/Backend/Services/ChatService.API/Publishers/NotificationPublisher.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Publishers/NotificationPublisher.cs similarity index 82% rename from Fitness/Backend/Services/ChatService.API/Publishers/NotificationPublisher.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Publishers/NotificationPublisher.cs index 070b828..85426e7 100644 --- a/Fitness/Backend/Services/ChatService.API/Publishers/NotificationPublisher.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Publishers/NotificationPublisher.cs @@ -16,18 +16,17 @@ public NotificationPublisher(IPublishEndpoint publishEndpoint, IMapper mapper) _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } - public async Task PublishNotification(string title, string content, string type, bool email, string userId, string userType) + public async Task PublishNotification(string title, string content, string type, bool email, + IDictionary users) { - Enum.TryParse(userType, out Notification.UserType uType); Enum.TryParse(type, out Notification.NotificationType nType); var notification = new Notification { + UserIdToUserType = users, Title = title, Content = content, - NType = nType, + Type = nType, Email = email, - UserId = userId, - UType = uType }; var eventMessage = _mapper.Map(notification); diff --git a/Fitness/Backend/Services/ChatService.API/Repositories/ChatRepository.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Repositories/ChatRepository.cs similarity index 81% rename from Fitness/Backend/Services/ChatService.API/Repositories/ChatRepository.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Repositories/ChatRepository.cs index 0788288..04d0e49 100644 --- a/Fitness/Backend/Services/ChatService.API/Repositories/ChatRepository.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Repositories/ChatRepository.cs @@ -57,11 +57,23 @@ public async Task DeleteChatSessionAsync(string trainerId, string clientId public async Task ExtendChatSessionAsync(string sessionId) { var filter = Builders.Filter.Eq(s => s.Id, new ObjectId(sessionId)); - var update = Builders.Update - .Set(s => s.IsUnlocked, true) - .Set(s => s.ExpirationDate, DateTime.UtcNow.AddDays(30)); + var updatePipeline = new PipelineUpdateDefinition( + new[] + { + new BsonDocument("$set", new BsonDocument + { + { "IsUnlocked", true }, + { + "ExpirationDate", new BsonDocument("$add", new BsonArray + { + new BsonDocument("$max", new BsonArray { "$ExpirationDate", DateTime.UtcNow }), + (long)TimeSpan.FromDays(30).TotalMilliseconds + }) + } + }) + }); - var result = await _chatSessions.UpdateOneAsync(filter, update); + var result = await _chatSessions.UpdateOneAsync(filter, updatePipeline); return result.ModifiedCount > 0; } diff --git a/Fitness/Backend/Services/ChatService.API/Repositories/IChatRepository.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Repositories/IChatRepository.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Repositories/IChatRepository.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Repositories/IChatRepository.cs diff --git a/Fitness/Backend/Services/ChatService.API/Services/ChatService.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Services/ChatService.cs similarity index 93% rename from Fitness/Backend/Services/ChatService.API/Services/ChatService.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Services/ChatService.cs index bc779b7..1ba8565 100644 --- a/Fitness/Backend/Services/ChatService.API/Services/ChatService.cs +++ b/Fitness/Backend/Services/ChatService/ChatService.API/Services/ChatService.cs @@ -86,11 +86,13 @@ public async Task CreateChatSessionAsync(string trainerId, string clientId) Messages = CreateInitialMessages() }; await _chatRepository.InsertChatSessionAsync(session); + + var users = new Dictionary(); + users.Add(clientId, "Client"); + users.Add(trainerId, "Trainer"); - await Task.WhenAll( - _notificationPublisher.PublishNotification("Chat Session Created", "New chat session created!", "Information", true, clientId, "Client"), - _notificationPublisher.PublishNotification("Chat Session Created", "New chat session created!", "Information", true, trainerId, "Trainer") - ); + await _notificationPublisher.PublishNotification("Chat Session Created", "New chat session created!", + "Information", true, users); } public async Task GetChatSessionAsync(string trainerId, string clientId) diff --git a/Fitness/Backend/Services/ChatService.API/Services/IChatService.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Services/IChatService.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Services/IChatService.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Services/IChatService.cs diff --git a/Fitness/Backend/Services/ChatService.API/Services/WebSocketHandler.cs b/Fitness/Backend/Services/ChatService/ChatService.API/Services/WebSocketHandler.cs similarity index 100% rename from Fitness/Backend/Services/ChatService.API/Services/WebSocketHandler.cs rename to Fitness/Backend/Services/ChatService/ChatService.API/Services/WebSocketHandler.cs diff --git a/Fitness/Backend/Services/ChatService.API/appsettings.Development.json b/Fitness/Backend/Services/ChatService/ChatService.API/appsettings.Development.json similarity index 75% rename from Fitness/Backend/Services/ChatService.API/appsettings.Development.json rename to Fitness/Backend/Services/ChatService/ChatService.API/appsettings.Development.json index 4697dde..791447c 100644 --- a/Fitness/Backend/Services/ChatService.API/appsettings.Development.json +++ b/Fitness/Backend/Services/ChatService/ChatService.API/appsettings.Development.json @@ -11,6 +11,12 @@ "ChatSessionsCollection": "ChatSessions", "MessagesCollection": "Messages" }, + "JwtSettings": { + "validIssuer": "Fitness Identity", + "validAudience": "Fitness", + "secretKey": "MyVeryVerySecretMessageForSecretKey", + "expires": 15 + }, "ConsulConfig": { "Address": "http://consul:8500", "ServiceName": "ChatService.API", @@ -18,6 +24,4 @@ "ServiceAddress": "chatservice.api", "ServicePort": 8080 } - - } diff --git a/Fitness/Backend/Services/ChatService/ChatService.API/appsettings.json b/Fitness/Backend/Services/ChatService/ChatService.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Fitness/Backend/Services/ChatService/ChatService.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Fitness/Backend/Services/ClientService/ClientService.API/Controllers/ClientController.cs b/Fitness/Backend/Services/ClientService/ClientService.API/Controllers/ClientController.cs index d0ec818..5ae2cce 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.API/Controllers/ClientController.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.API/Controllers/ClientController.cs @@ -1,30 +1,27 @@ using AutoMapper; using ClientService.Common.Entities; using ClientService.Common.Repositories; -using EventBus.Messages.Events; using MassTransit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace ClientService.API.Controllers { - // [Authorize] + [Authorize] [ApiController] [Route("api/v1/[controller]")] public class ClientController:ControllerBase { private readonly IRepository _repository; private readonly IMapper _mapper; - private readonly IPublishEndpoint _publishEndpoint; - public ClientController(IRepository repository, IMapper mapper, IPublishEndpoint publishEndpoint) + public ClientController(IRepository repository, IMapper mapper) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); } - // [Authorize(Roles = "Admin, Trainer")] + [Authorize(Roles = "Admin, Trainer")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetClients() @@ -33,7 +30,7 @@ public async Task>> GetClients() return Ok(clients); } - // [Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet("{id}", Name = "GetClient")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -49,7 +46,7 @@ public async Task> GetClientById(string id) // TODO("Dodati GetClientsByIds - mozda gRPC!!!") - // [Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [Route("[action]/{name}")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] @@ -59,7 +56,7 @@ public async Task>> GetClientsByName(string nam return Ok(results); } - // [Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [Route("[action]/{surname}")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] @@ -69,7 +66,7 @@ public async Task>> GetClientsBySurname(string return Ok(results); } - // [Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [Route("[action]/{email}")] [HttpGet] [ProducesResponseType(typeof(Client), StatusCodes.Status200OK)] @@ -87,7 +84,7 @@ public async Task> CreateClient([FromBody] Client client) return CreatedAtRoute("GetClient", new { id = client.Id }, client); } - // [Authorize(Roles = "Admin, Client")] + [Authorize(Roles = "Admin, Client")] [HttpPut] [ProducesResponseType(typeof(Client), StatusCodes.Status200OK)] public async Task UpdateClient([FromBody] Client client) @@ -95,7 +92,7 @@ public async Task UpdateClient([FromBody] Client client) return Ok(await _repository.UpdateClient(client)); } - // [Authorize(Roles = "Admin")] + [Authorize(Roles = "Admin")] [HttpDelete("{id}", Name = "DeleteClient")] [ProducesResponseType(typeof(Client), StatusCodes.Status200OK)] public async Task DeleteClient(string id) @@ -103,7 +100,7 @@ public async Task DeleteClient(string id) return Ok(await _repository.DeleteClient(id)); } - // [Authorize(Roles = "Admin")] + [Authorize(Roles = "Admin")] [Route("[action]")] [HttpDelete] [ProducesResponseType(typeof(Client), StatusCodes.Status200OK)] @@ -112,79 +109,5 @@ public async Task DeleteAllClients() await _repository.DeleteAllClients(); return Ok(); } - - // [Authorize(Roles = "Client")] - [Route("[action]/{id}")] - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetClientScheduleByClientId(string id) - { - var result = await _repository.GetClientScheduleByClientId(id); - if (result == null) - { - return NotFound(); - } - return Ok(result); - } - - // [Authorize(Roles = "Client")] - [Route("[action]/{clientId}/{weekId}")] - [HttpGet] - [ProducesResponseType(typeof(WeeklySchedule), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetClientWeekSchedule(string clientId, int weekId) - { - var result = await _repository.GetClientWeekSchedule(clientId, weekId); - if (result == null) - { - return NotFound(); - } - return Ok(result); - } - - // [Authorize(Roles = "Client")] - [Route("[action]/{clientId}")] - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetTrainerIdsFromClientSchedule(string clientId) - { - var result = await _repository.GetTrainerIdsFromClientSchedule(clientId); - if (result == null || !result.Any()) - { - return NotFound(); - } - return Ok(result); - } - - // [Authorize(Roles = "Client")] - [Route("[action]")] - [HttpPut] - [ProducesResponseType(typeof(ClientSchedule), StatusCodes.Status200OK)] - public async Task UpdateClientSchedule([FromBody] ClientSchedule clientSchedule) - { - return Ok(await _repository.UpdateClientSchedule(clientSchedule)); - - } - - // [Authorize(Roles = "Client")] - [Route("[action]")] - [HttpPut] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task BookTraining([FromBody] BookTrainingInformation bti) - { - var result = await _repository.BookTraining(bti); - if (result) - { - //send to trainer - - var eventMessage = _mapper.Map(bti); - await _publishEndpoint.Publish(eventMessage); - return Ok(); - } - return BadRequest(); - } } } diff --git a/Fitness/Backend/Services/ClientService/ClientService.API/EventBusConsumers/CancelTrainingConsumer.cs b/Fitness/Backend/Services/ClientService/ClientService.API/EventBusConsumers/CancelTrainingConsumer.cs deleted file mode 100644 index 6f352bd..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.API/EventBusConsumers/CancelTrainingConsumer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AutoMapper; -using ClientService.Common.Entities; -using ClientService.Common.Repositories; -using EventBus.Messages.Events; -using MassTransit; - -namespace ClientService.API.EventBusConsumers -{ - public class CancelTrainingConsumer : IConsumer - { - private readonly IRepository _repository; - private readonly IMapper _mapper; - - public CancelTrainingConsumer(IRepository repository, IMapper mapper) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - } - - public async Task Consume(ConsumeContext context) - { - var cti = _mapper.Map(context.Message); - await _repository.CancelledTrainingByTrainer(cti); - } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/BookTrainingProfile.cs b/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/BookTrainingProfile.cs deleted file mode 100644 index eb6d844..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/BookTrainingProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; -using ClientService.Common.Entities; -using EventBus.Messages.Events; - -namespace ClientService.API.Mapper -{ - public class BookTrainingProfile : Profile - { - public BookTrainingProfile() - { - CreateMap().ReverseMap(); - } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/CancelTrainingProfile.cs b/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/CancelTrainingProfile.cs deleted file mode 100644 index 72644e7..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.API/Mapper/CancelTrainingProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; -using ClientService.Common.Entities; -using EventBus.Messages.Events; - -namespace ClientService.API.Mapper -{ - public class CancelTrainingProfile:Profile - { - public CancelTrainingProfile() - { - CreateMap().ReverseMap(); - } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.API/Program.cs b/Fitness/Backend/Services/ClientService/ClientService.API/Program.cs index 3a9b595..41b0292 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.API/Program.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.API/Program.cs @@ -1,10 +1,5 @@ -using Amazon.Runtime.Internal; -using ClientService.API.EventBusConsumers; -using EventBus.Messages.Constants; -using MassTransit; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; -// using Microsoft.IdentityModel.Tokens; using System.Reflection; using System.Text; using Consul; @@ -44,20 +39,6 @@ //AutoMapper builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); -//EventBus -builder.Services.AddMassTransit(config => -{ - config.AddConsumer(); - config.UsingRabbitMq((ctx, cfg) => - { - cfg.Host(builder.Configuration["EventBusSettings:HostAddress"]); - cfg.ReceiveEndpoint(EventBusConstants.CancellingTrainingQueue, c => - { - c.ConfigureConsumer(ctx); - }); - }); -}); - var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var secretKey = jwtSettings.GetValue("secretKey"); @@ -114,10 +95,11 @@ consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); }); -// app.UseAuthentication(); -// app.UseAuthorization(); - app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.Run(); diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Data/Context.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Data/Context.cs index 8987460..eef1e4a 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Data/Context.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.Common/Data/Context.cs @@ -10,13 +10,11 @@ public Context(IConfiguration configuration) { var client = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString")); - var database = client.GetDatabase("ClientsAndSchedules"); + var database = client.GetDatabase("ClientDB"); Clients = database.GetCollection("Clients"); - ClientSchedules = database.GetCollection("ClientSchedules"); } public IMongoCollection Clients { get; } - public IMongoCollection ClientSchedules { get; } } } diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Data/IContext.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Data/IContext.cs index 1b76315..902804f 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Data/IContext.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.Common/Data/IContext.cs @@ -6,6 +6,5 @@ namespace ClientService.Common.Data public interface IContext { IMongoCollection Clients { get; } - IMongoCollection ClientSchedules { get; } } } diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/BookTrainingInformation.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/BookTrainingInformation.cs deleted file mode 100644 index 11640cd..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/BookTrainingInformation.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ClientService.Common.Entities -{ - public class BookTrainingInformation - { - public string ClientId { get; set; } - public string TrainerName { get; set; } - public string TrainerId { get; set; } - public string TrainingType { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - public bool IsBooking { get; set; } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/CancelTrainingInformation.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/CancelTrainingInformation.cs deleted file mode 100644 index e82e0de..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/CancelTrainingInformation.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace ClientService.Common.Entities -{ - public class CancelTrainingInformation - { - public string ClientId { get; set; } - public string TrainerId { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ClientSchedule.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ClientSchedule.cs deleted file mode 100644 index e66647f..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ClientSchedule.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson; -using Microsoft.Extensions.Configuration; - -namespace ClientService.Common.Entities -{ - public class ClientSchedule - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - public string Id { get; set; } - - [BsonElement("ClientId")] - public string ClientId { get; set; } - public List WeeklySchedules { get; set; } = new List(); - public ClientSchedule(string id) - { - var startWeek = 1; - var endWeek = 3; - ClientId = id; - for (int i = startWeek; i <= endWeek; i++) - { - WeeklySchedules.Add(new WeeklySchedule(i)); - } - } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ScheduleItem.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ScheduleItem.cs deleted file mode 100644 index a767fef..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/ScheduleItem.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace ClientService.Common.Entities -{ - public class ScheduleItem - { - - public int StartHour { get; set; } - public int StartMinute { get; set; } - public int EndHour { get; set; } - public int EndMinute { get; set; } - public string TrainingType { get; set; } - public bool IsAvailable { get; set; } - public string TrainerId { get; set; } - public string TrainerName { get; set; } - public int TrainingStartHour { get; set; } - public int TrainingStartMinute { get; set; } - public TimeSpan TrainingDuration { get; set; } - - public ScheduleItem(TimeSpan startTime, TimeSpan endTime,bool isAvailable) - { - - StartHour = startTime.Hours; - StartMinute = startTime.Minutes; - EndHour = endTime.Hours; - EndMinute = endTime.Minutes; - IsAvailable = isAvailable; - TrainingType = ""; - TrainerId = ""; - TrainerName = ""; - } - - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/WeeklySchedule.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/WeeklySchedule.cs deleted file mode 100644 index 39a0630..0000000 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Entities/WeeklySchedule.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace ClientService.Common.Entities -{ - public class WeeklySchedule - { - public int WeekId { get; set; } - public Dictionary> DailySchedules { get; set; } = new Dictionary>(); - - private static readonly Dictionary DayName = new Dictionary - { - { 1, "Monday" }, - { 2, "Tuesday" }, - { 3, "Wednesday" }, - { 4, "Thursday" }, - { 5, "Friday" }, - { 6, "Saturday" }, - { 7, "Sunday" } - }; - public WeeklySchedule(int weekId) - { - WeekId = weekId; - - for (int i = 1; i <= 7; ++i) - { - DailySchedules[DayName[i]] = InitializeDay(); - } - } - - private static List InitializeDay() - { - var timeslots = new List(); - var startTime = new TimeSpan(8, 0, 0); // 8:00 AM - var endTime = new TimeSpan(20, 0, 0); // 8:00 PM - - while (startTime < endTime) - { - var nextTime = startTime.Add(TimeSpan.FromMinutes(15)); - timeslots.Add(new ScheduleItem(startTime,nextTime,true)); - startTime = nextTime; - } - return timeslots; - } - } -} diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/IRepository.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/IRepository.cs index ede3d9c..b1cdee6 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/IRepository.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/IRepository.cs @@ -14,11 +14,5 @@ public interface IRepository Task UpdateClient(Client client); Task DeleteClient(string id); Task DeleteAllClients(); - Task GetClientScheduleByClientId(string id); - Task GetClientWeekSchedule(string clientId, int weekId); - Task> GetTrainerIdsFromClientSchedule(string clientId); - Task UpdateClientSchedule(ClientSchedule clientSchedule); - Task BookTraining(BookTrainingInformation bti); - Task CancelledTrainingByTrainer(CancelTrainingInformation cti); } } diff --git a/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/Repository.cs b/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/Repository.cs index 3c3c959..647ec87 100644 --- a/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/Repository.cs +++ b/Fitness/Backend/Services/ClientService/ClientService.Common/Repositories/Repository.cs @@ -42,8 +42,6 @@ public async Task> GetClientsBySurname(string surname) public async Task CreateClient(Client client) { await _context.Clients.InsertOneAsync(client); - var clientSchedule = new ClientSchedule(client.Id); - await _context.ClientSchedules.InsertOneAsync(clientSchedule); } public async Task UpdateClient(Client client) { @@ -53,125 +51,11 @@ public async Task UpdateClient(Client client) public async Task DeleteClient(string id) { var resultClient = await _context.Clients.DeleteOneAsync(p => p.Id == id); - var resultSchedule = await _context.ClientSchedules.DeleteOneAsync(p => p.ClientId == id); - - return resultClient.IsAcknowledged && resultClient.DeletedCount > 0 - && resultSchedule.IsAcknowledged && resultSchedule.DeletedCount>0; + return resultClient.IsAcknowledged && resultClient.DeletedCount > 0; } public async Task DeleteAllClients() { await _context.Clients.DeleteManyAsync(p => true); } - - public async Task GetClientScheduleByClientId(string id) - { - return await _context.ClientSchedules.Find(s => s.ClientId == id).FirstOrDefaultAsync(); - } - public async Task GetClientWeekSchedule(string clientId, int weekId) - { - var clientSchedule = await GetClientScheduleByClientId(clientId); - return clientSchedule?.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == weekId); - } - - public async Task> GetTrainerIdsFromClientSchedule(string clientId) - { - var clientSchedule = await GetClientScheduleByClientId(clientId); - - if (clientSchedule == null) - { - return Enumerable.Empty(); - } - - // Assuming `clientSchedule` contains a collection of training slots - var trainerIds = clientSchedule.WeeklySchedules - .SelectMany(ws => ws.DailySchedules.Values) - .SelectMany(dayList => dayList) - .Select(si => si.TrainerId) - .Where(id => !string.IsNullOrEmpty(id)) - .Distinct(); - - return trainerIds; - } - - public async Task UpdateClientSchedule(ClientSchedule clientSchedule) - { - var result = await _context.ClientSchedules.ReplaceOneAsync(cs => cs.ClientId == clientSchedule.ClientId, clientSchedule, new ReplaceOptions { IsUpsert = true }); - return result.IsAcknowledged && result.ModifiedCount > 0; - } - - public async Task BookTraining(BookTrainingInformation bti) - { - var clientSchedule = await GetClientScheduleByClientId(bti.ClientId); - if (clientSchedule == null) - { - return false; - } - - var weeklySchedule = clientSchedule.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == bti.WeekId); - if (weeklySchedule == null) - { - return false; - } - - if (!weeklySchedule.DailySchedules.TryGetValue(bti.DayName, out var dailySchedule)) - return false; - - int numberOfCells = (int)bti.Duration.TotalMinutes / 15; - - - var startSlotIndex = dailySchedule.FindIndex(slot => slot.StartHour == bti.StartHour && slot.StartMinute == bti.StartMinute); - - if (startSlotIndex == -1 || startSlotIndex + numberOfCells > dailySchedule.Count) - return false; - - for (int i = startSlotIndex; i < startSlotIndex + numberOfCells; i++) - { - if (bti.IsBooking) - { - if (!dailySchedule[i].IsAvailable) - return false; - - dailySchedule[i].IsAvailable = false; - dailySchedule[i].TrainerId = bti.TrainerId; - dailySchedule[i].TrainingType = bti.TrainingType; - dailySchedule[i].TrainerName = bti.TrainerName; - dailySchedule[i].TrainingStartHour = bti.StartHour; - dailySchedule[i].TrainingDuration = bti.Duration; - dailySchedule[i].TrainingStartMinute = bti.StartMinute; - } - else - { - dailySchedule[i].IsAvailable = true; - dailySchedule[i].TrainerId = ""; - dailySchedule[i].TrainingType = ""; - dailySchedule[i].TrainingStartHour = -1; - } - } - - await UpdateClientSchedule(clientSchedule); - - return true; - } - public async Task CancelledTrainingByTrainer(CancelTrainingInformation cti) - { - //obavlja se validacija na frontu - var clientSchedule = await GetClientScheduleByClientId(cti.ClientId); - var weeklySchedule = clientSchedule.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == cti.WeekId); - weeklySchedule.DailySchedules.TryGetValue(cti.DayName, out var dailySchedule); - - int numberOfCells = (int)cti.Duration.TotalMinutes / 15; - var startSlotIndex = dailySchedule.FindIndex(slot => slot.StartHour == cti.StartHour && slot.StartMinute == cti.StartMinute); - - for (int i = startSlotIndex; i < startSlotIndex + numberOfCells; i++) - { - dailySchedule[i].IsAvailable = true; - dailySchedule[i].TrainerId = ""; - dailySchedule[i].TrainingType = ""; - dailySchedule[i].TrainingStartHour = -1; - } - - await UpdateClientSchedule(clientSchedule); - } - } } diff --git a/Fitness/Backend/Services/GatewayService/GatewayService.API/ocelot.json b/Fitness/Backend/Services/GatewayService/GatewayService.API/ocelot.json index 408ab11..86ad087 100644 --- a/Fitness/Backend/Services/GatewayService/GatewayService.API/ocelot.json +++ b/Fitness/Backend/Services/GatewayService/GatewayService.API/ocelot.json @@ -5,45 +5,45 @@ "DownstreamScheme": "http", "ServiceName": "ClientService.API", "UpstreamPathTemplate": "/client/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { "DownstreamPathTemplate": "/api/v1/trainer/{everything}", "DownstreamScheme": "http", "ServiceName": "TrainerService.API", "UpstreamPathTemplate": "/trainer/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { "DownstreamPathTemplate": "/api/v1/payment/{everything}", "DownstreamScheme": "http", "ServiceName": "PaymentService.API", "UpstreamPathTemplate": "/payment/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { "DownstreamPathTemplate": "/api/v1/review/{everything}", "DownstreamScheme": "http", "ServiceName": "ReviewService.API", "UpstreamPathTemplate": "/review/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { "DownstreamPathTemplate": "/api/v1/authentication/{everything}", "DownstreamScheme": "http", "ServiceName": "IdentityServer", "UpstreamPathTemplate": "/authentication/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { "DownstreamPathTemplate": "/api/v1/user/{everything}", "DownstreamScheme": "http", "ServiceName": "IdentityServer", "UpstreamPathTemplate": "/user/{everything}", - "UpstreamHttpMethod": ["GET", "POST", "PUT"] + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] }, { - "DownstreamPathTemplate": "/api/chat/{everything}", + "DownstreamPathTemplate": "/api/v1/chat/{everything}", "DownstreamScheme": "http", "ServiceName": "ChatService.API", "UpstreamPathTemplate": "/chat/{everything}", @@ -55,6 +55,62 @@ "ServiceName": "NotificationService.API", "UpstreamPathTemplate": "/notification/{everything}", "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/analytics/{everything}", + "DownstreamScheme": "http", + "ServiceName": "AnalyticsService.API", + "UpstreamPathTemplate": "/analytics/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/reservation/{everything}", + "DownstreamScheme": "http", + "ServiceName": "ReservationService.API", + "UpstreamPathTemplate": "/reservation/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/training/{everything}", + "DownstreamScheme": "http", + "ServiceName": "VideoTrainingService.API", + "UpstreamPathTemplate": "/training/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/upload/{everything}", + "DownstreamScheme": "http", + "ServiceName": "VideoTrainingService.API", + "UpstreamPathTemplate": "/upload/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/food/{everything}", + "DownstreamScheme": "http", + "ServiceName": "NutritionService.API", + "UpstreamPathTemplate": "/food/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/goals/{everything}", + "DownstreamScheme": "http", + "ServiceName": "NutritionService.API", + "UpstreamPathTemplate": "/goals/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/health/{everything}", + "DownstreamScheme": "http", + "ServiceName": "NutritionService.API", + "UpstreamPathTemplate": "/health/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] + }, + { + "DownstreamPathTemplate": "/api/v1/mealplans/{everything}", + "DownstreamScheme": "http", + "ServiceName": "NutritionService.API", + "UpstreamPathTemplate": "/mealplans/{everything}", + "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"] } ] } \ No newline at end of file diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Controller/NotificationController.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Controller/NotificationController.cs index b9645e5..009db14 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Controller/NotificationController.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Controller/NotificationController.cs @@ -1,4 +1,3 @@ -using System.Collections; using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -7,7 +6,7 @@ namespace NotificationService.API.Controller; -//[Authorize] +[Authorize] [ApiController] [Route("api/v1/[controller]")] public class NotificationController : ControllerBase @@ -19,7 +18,7 @@ public NotificationController(IRepository repository, IMapper mapper) _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } - //[Authorize(Roles = "Admin")] + [Authorize(Roles = "Admin")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetNotifications() @@ -28,7 +27,7 @@ public async Task>> GetNotifications() return Ok(notifications); } - //[Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet("user/{userId}")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetNotificationsByUserId(string userId) @@ -37,7 +36,7 @@ public async Task>> GetNotificationsByUse return Ok(notifications); } - //[Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet("{id}")] [ProducesResponseType(typeof(Notification), StatusCodes.Status200OK)] public async Task> GetNotificationById(string id) @@ -46,7 +45,7 @@ public async Task> GetNotificationById(string id) return Ok(notification); } - //[Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpPut] [ProducesResponseType(typeof(Notification), StatusCodes.Status200OK)] public async Task UpdateNotification([FromBody] Notification notification) @@ -54,7 +53,23 @@ public async Task UpdateNotification([FromBody] Notification noti return Ok(await _repository.UpdateNotification(notification)); } - //[Authorize(Roles = "Admin")] + // PUT api/notifications/{id}/read + [Authorize(Roles = "Trainer, Client")] + [HttpPut("{id}/read")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task MarkAsRead(string id) + { + var success = await _repository.MarkNotificationAsRead(id); + + if (!success) + return NotFound(); + + return Ok(new { Message = "Notification marked as read" }); + } + + + [Authorize(Roles = "Admin")] [HttpDelete] [ProducesResponseType(typeof(Notification), StatusCodes.Status200OK)] public async Task DeleteNotifications() @@ -63,7 +78,7 @@ public async Task DeleteNotifications() return Ok(); } - //[Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpDelete("/user/{userId}")] [ProducesResponseType(typeof(Notification), StatusCodes.Status200OK)] public async Task DeleteNotificationsByUserTypeAndUserId(string userId) @@ -71,7 +86,7 @@ public async Task DeleteNotificationsByUserTypeAndUserId(string u return Ok( await _repository.DeleteNotificationsByUserId(userId)); } - //[Authorize(Roles = "Admin, Trainer, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpDelete("{id}")] [ProducesResponseType(typeof(Notification), StatusCodes.Status200OK)] public async Task DeleteNotification(string id) diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/Notification.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/Notification.cs index 8e11e21..4fce431 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/Notification.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/Notification.cs @@ -9,11 +9,10 @@ public class Notification [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } public DateTime CreationDate { get; set; } - public string UserId { get; set; } - public string UType { get; set; } + public IDictionary UserIdToUserType; public string Title { get; set; } public string Content { get; set; } - public string NType { get; set; } + public string Type { get; set; } public bool Email { get; set; } public bool NotificationRead { get; set; } = false; } \ No newline at end of file diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/NotificationReceived.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/NotificationReceived.cs index 881ab39..9f5a914 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/NotificationReceived.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Entities/NotificationReceived.cs @@ -6,19 +6,11 @@ namespace NotificationService.API.Entities; public class NotificationReceived { public DateTime CreationDate { get; set; } - public string UserId { get; set; } - public UserType UType { get; set; } + public IDictionary UserIdToUserType; public string Title { get; set; } public string Content { get; set; } - public NotificationType NType { get; set; } + public NotificationType Type { get; set; } public bool Email { get; set; } - - public enum UserType - { - Client, - Trainer - } - public enum NotificationType { Information, diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/EventBusConsumers/NotificationConsumer.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/EventBusConsumers/NotificationConsumer.cs index bf5ae03..8aaf893 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/EventBusConsumers/NotificationConsumer.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/EventBusConsumers/NotificationConsumer.cs @@ -1,6 +1,8 @@ +using System.Text.Json; using AutoMapper; using MassTransit; using EventBus.Messages.Events; +using MongoDB.Bson; using NotificationService.API.Email; using NotificationService.API.Entities; using NotificationService.API.GrpcServices; @@ -35,17 +37,20 @@ public async Task Consume(ConsumeContext context) var subject = "[FitPlusPlus Gym] " + notification.Title; var body = "You have a new notification from your FitPlusPlus Gym Account:\n\n" + notification.Content + "\n\nTime: " + notification.CreationDate; - - if (notification.UType == NotificationReceived.UserType.Client) + foreach (var (key, value) in notification.UserIdToUserType) { - var client = await _clientGrpcService.GetClient(notification.UserId); - var clientInfo = _mapper.Map(client); - await _emailService.SendEmailAsync(clientInfo.Email, subject, body); - } else if (notification.UType == NotificationReceived.UserType.Trainer) - { - var trainer = await _trainerGrpcService.GetTrainers(notification.UserId); - var trainerInfo = _mapper.Map(trainer); - await _emailService.SendEmailAsync(trainerInfo.ContactEmail, subject, body); + if (value.Equals("Client")) + { + var client = await _clientGrpcService.GetClient(key); + var clientInfo = _mapper.Map(client); + await _emailService.SendEmailAsync(clientInfo.Email, subject, body); + } + else if (value.Equals("Trainer")) + { + var trainer = await _trainerGrpcService.GetTrainers(key); + var trainerInfo = _mapper.Map(trainer); + await _emailService.SendEmailAsync(trainerInfo.ContactEmail, subject, body); + } } } } diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Program.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Program.cs index 7c36c6f..ba65f41 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Program.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Program.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Net.Mail; using System.Text; using ClientService.GRPC.Protos; using Consul; @@ -50,7 +48,6 @@ builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); }); -Console.WriteLine(Environment.GetEnvironmentVariable("EMAIL_PASSWORD")); var options = new SmtpClientOptions { Server = emailSettings["SmtpHost"]!, @@ -96,7 +93,6 @@ var jwtSettings = builder.Configuration.GetSection("JwtSettings"); var secretKey = jwtSettings.GetValue("secretKey")!; -/* builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; @@ -116,7 +112,6 @@ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) }; }); - */ var app = builder.Build(); @@ -152,8 +147,8 @@ app.UseRouting(); -//app.UseAuthentication(); -//app.UseAuthorization(); +app.UseAuthentication(); +app.UseAuthorization(); app.MapControllers(); diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/IRepository.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/IRepository.cs index f139c80..04d18e5 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/IRepository.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/IRepository.cs @@ -14,6 +14,8 @@ public interface IRepository Task UpdateNotification(Notification notification); + Task MarkNotificationAsRead(string notificationId); + Task DeleteNotification(string id); Task DeleteNotificationsByUserId(string userId); diff --git a/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/Repository.cs b/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/Repository.cs index 0c4f266..51dcfee 100644 --- a/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/Repository.cs +++ b/Fitness/Backend/Services/NotificationService/NotificationService.API/Repositories/Repository.cs @@ -20,7 +20,7 @@ public async Task> GetNotifications() public async Task> GetNotificationsByUserId(string userId) { - return await _context.Notifications.Find(n => n.UserId == userId).ToListAsync(); + return await _context.Notifications.Find(n => n.UserIdToUserType.ContainsKey(userId)).ToListAsync(); } public async Task GetNotificationById(string id) @@ -38,6 +38,18 @@ public async Task UpdateNotification(Notification notification) var result = await _context.Notifications.ReplaceOneAsync(n => n.Id == notification.Id, notification); return result.IsAcknowledged && result.ModifiedCount > 0; } + + public async Task MarkNotificationAsRead(string notificationId) + { + var filter = Builders.Filter.Eq(n => n.Id, notificationId); + + var update = Builders.Update + .Set(n => n.NotificationRead, true); // menjaš polje IsRead + + var result = await _context.Notifications.UpdateOneAsync(filter, update); + + return result.IsAcknowledged && result.ModifiedCount > 0; + } public async Task DeleteNotification(string id) { @@ -47,7 +59,7 @@ public async Task DeleteNotification(string id) public async Task DeleteNotificationsByUserId(string userId) { - var result = await _context.Notifications.DeleteManyAsync(n => n.UserId == userId); + var result = await _context.Notifications.DeleteManyAsync(n => n.UserIdToUserType.ContainsKey(userId)); return result.IsAcknowledged && result.DeletedCount > 0; } diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/FoodController.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/FoodController.cs new file mode 100644 index 0000000..bb1c3cf --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/FoodController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Driver; +using NutritionService.API.Models; + +namespace NutritionService.API.Controllers +{ + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + public class FoodController : ControllerBase + { + private readonly IMongoCollection _foods; + + public FoodController(IMongoDatabase db) + { + _foods = db.GetCollection("Food"); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPost] + public async Task AddFood([FromBody] Food food) + { + if (string.IsNullOrWhiteSpace(food.Name)) + return BadRequest("Name is required"); + + await _foods.InsertOneAsync(food); + return Ok(food); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet] + public async Task GetAllFoods() + { + var foods = await _foods.Find(_ => true).ToListAsync(); + return Ok(foods); + } + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/GoalsController.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/GoalsController.cs new file mode 100644 index 0000000..4ae4ba5 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/GoalsController.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Driver; +using NutritionService.API.Models; + +namespace NutritionService.API.Controllers +{ + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + public class GoalsController : ControllerBase + { + private readonly IMongoCollection _goals; + + public GoalsController(IMongoDatabase db) + { + _goals = db.GetCollection("Goals"); + } + + [Authorize(Roles = "Client")] + [HttpPost] + public async Task SetGoal([FromBody] UserGoal goal) + { + int bmr = + goal.Sex == "male" + ? (int)(10 * goal.CurrentWeight + 6.25 * goal.Height - 5 * goal.Age + 5) + : (int)(10 * goal.CurrentWeight + 6.25 * goal.Height - 5 * goal.Age - 161); + + double activityFactor = goal.ActivityLevel switch + { + "sedentary" => 1.2, + "light" => 1.375, + "moderate" => 1.55, + "active" => 1.725, + "veryActive" => 1.9, + _ => 1.2, + }; + int tdee = (int)(bmr * activityFactor); + + int adjust = goal.Intensity switch + { + "low" => 300, + "medium" => 500, + "high" => 700, + _ => 500, + }; + + goal.TargetKcal = goal.GoalType switch + { + "lose" => Math.Max(1200, tdee - adjust), + "gain" => tdee + adjust, + "maintain" => tdee, + _ => tdee, + }; + + var h = goal.Height / 100.0; + goal.BMI = Math.Round(goal.CurrentWeight / (h * h), 2); + + if (string.IsNullOrWhiteSpace(goal.ClientId)) + { + goal.ClientId = Guid.NewGuid().ToString(); + } + + await _goals.InsertOneAsync(goal); + return Ok(goal); + } + + [Authorize(Roles = "Client")] + [HttpGet("plan/{clientId}")] + public async Task GetPlan(string clientId) + { + var goal = await _goals + .Find(g => g.ClientId == clientId) + .SortByDescending(g => g.Id) + .FirstOrDefaultAsync(); + + if (goal == null) + return NotFound("No goal found for this client."); + return Ok(goal); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("all")] + public async Task GetAllGoals() + { + var allGoals = await _goals.Find(_ => true).ToListAsync(); + + if (allGoals == null || allGoals.Count == 0) + return NotFound("No goals found."); + + var simplified = allGoals.Select(g => new + { + g.ClientId, + g.BMI, + g.TargetKcal, + }); + + return Ok(simplified); + } + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/HealthController.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/HealthController.cs new file mode 100644 index 0000000..a8add4e --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/HealthController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace NutritionService.API.Controllers +{ + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + public class HealthController : ControllerBase + { + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet] + public IActionResult Get() => Ok("NutritionService running.."); + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/MealPlansController.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/MealPlansController.cs new file mode 100644 index 0000000..cec4a72 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Controllers/MealPlansController.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Driver; +using NutritionService.API.Models; + +namespace NutritionService.API.Controllers +{ + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + public class MealPlansController : ControllerBase + { + private readonly IMongoCollection _plans; + private readonly IMongoCollection _foods; + + public MealPlansController(IMongoDatabase db) + { + _plans = db.GetCollection("MealPlans"); + _foods = db.GetCollection("Food"); + } + + [Authorize(Roles = "Trainer")] + [HttpPost] + public async Task CreatePlan([FromBody] MealPlan plan) + { + if (string.IsNullOrEmpty(plan.TrainerId) || string.IsNullOrEmpty(plan.TrainerName)) + return BadRequest(new { message = "TrainerId and TrainerName are required." }); + + if (string.IsNullOrEmpty(plan.GoalType)) + return BadRequest(new { message = "GoalType is required." }); + + async Task> FillFoodsAsync(List foods) + { + var filled = new List(); + foreach (var f in foods) + { + var dbFood = await _foods.Find(x => x.Name == f.Name).FirstOrDefaultAsync(); + if (dbFood != null) + filled.Add(dbFood); + } + return filled; + } + + plan.Breakfast = await FillFoodsAsync(plan.Breakfast); + plan.Lunch = await FillFoodsAsync(plan.Lunch); + plan.Dinner = await FillFoodsAsync(plan.Dinner); + plan.Snacks = await FillFoodsAsync(plan.Snacks); + plan.CreatedAt = DateTime.UtcNow; + + + var existing = await _plans + .Find(p => p.TrainerId == plan.TrainerId && p.GoalType == plan.GoalType) + .FirstOrDefaultAsync(); + + if (existing != null) + { + await _plans.DeleteOneAsync(p => p.Id == existing.Id); + } + + await _plans.InsertOneAsync(plan); + return Ok(plan); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("trainer/{trainerId}/goal/{goalType}")] + public async Task GetPlanByTrainerAndGoal(string trainerId, string goalType) + { + var plan = await _plans + .Find(p => p.TrainerId == trainerId && p.GoalType == goalType) + .SortByDescending(p => p.CreatedAt) + .FirstOrDefaultAsync(); + + if (plan == null) + return NotFound(new { message = "No plan found for this trainer and goal type." }); + + return Ok(plan); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet] + public async Task GetAllPlans() + { + var plans = await _plans.Find(_ => true).ToListAsync(); + return Ok(plans); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("trainer/{trainerId}")] + public async Task GetPlansForTrainer(string trainerId) + { + var trainerPlans = await _plans.Find(p => p.TrainerId == trainerId).ToListAsync(); + + if (!trainerPlans.Any()) + return NotFound(new { message = "No plans found for this trainer." }); + + return Ok(trainerPlans); + } + + [Authorize(Roles = "Trainer")] + [HttpDelete("trainer/{trainerId}/goal/{goalType}")] + public async Task DeletePlan(string trainerId, string goalType) + { + var result = await _plans.DeleteOneAsync(p => + p.TrainerId == trainerId && p.GoalType == goalType + ); + + if (result.DeletedCount == 0) + return NotFound( + new + { + message = $"No plan found for trainer '{trainerId}' and goal '{goalType}'.", + } + ); + + return Ok( + new + { + message = $"Plan for trainer '{trainerId}' and goal '{goalType}' deleted successfully.", + } + ); + } + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Dockerfile b/Fitness/Backend/Services/NutritionService/NutritionService.API/Dockerfile new file mode 100644 index 0000000..c84425d --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Dockerfile @@ -0,0 +1,28 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY ["Services/NutritionService/NutritionService.API/NutritionService.API.csproj", "Services/NutritionService/NutritionService.API/"] +COPY ["Common/ConsulConfig.Settings/ConsulConfig.Settings.csproj", "Common/ConsulConfig.Settings/"] +COPY ["Common/EventBus.Messages/EventBus.Messages.csproj", "Common/EventBus.Messages/"] + + +RUN dotnet restore "Services/NutritionService/NutritionService.API/NutritionService.API.csproj" + +COPY . . + +WORKDIR "/src/Services/NutritionService/NutritionService.API" +RUN dotnet build "NutritionService.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "NutritionService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "NutritionService.API.dll"] + + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/Food.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/Food.cs new file mode 100644 index 0000000..5623e1d --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/Food.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace NutritionService.API.Models +{ + public class Food + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + public string Name { get; set; } + public int Calories { get; set; } + public double Protein { get; set; } + public double Carbs { get; set; } + public double Fat { get; set; } + } +} + + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/MealPlan.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/MealPlan.cs new file mode 100644 index 0000000..60d1c16 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/MealPlan.cs @@ -0,0 +1,27 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace NutritionService.API.Models +{ + public class MealPlan + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + public string TrainerId { get; set; } = string.Empty; + public string TrainerName { get; set; } = string.Empty; + public string GoalType { get; set; } = string.Empty; + + public List Breakfast { get; set; } = new(); + public List Lunch { get; set; } = new(); + public List Dinner { get; set; } = new(); + public List Snacks { get; set; } = new(); + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} + + + + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/UserGoal.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/UserGoal.cs new file mode 100644 index 0000000..69aec35 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Models/UserGoal.cs @@ -0,0 +1,25 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace NutritionService.API.Models +{ + public class UserGoal + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + public string ClientId { get; set; } + + // Podaci koje klijent unosi + public string Sex { get; set; } + public int Age { get; set; } + public double Height { get; set; } + public double CurrentWeight { get; set; } + public string ActivityLevel { get; set; } + public string GoalType { get; set; } + public string Intensity { get; set; } + + public int TargetKcal { get; set; } + public double BMI { get; set; } + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/NutritionService.API.csproj b/Fitness/Backend/Services/NutritionService/NutritionService.API/NutritionService.API.csproj new file mode 100644 index 0000000..34d66d9 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/NutritionService.API.csproj @@ -0,0 +1,23 @@ + + + net8.0 + enable + enable + Linux + ..\..\.. + ..\..\..\docker-compose.dcproj + + + + + + + + + + + + + + + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Program.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Program.cs new file mode 100644 index 0000000..54f8f6b --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Program.cs @@ -0,0 +1,98 @@ +using Consul; +using ConsulConfig.Settings; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using MongoDB.Driver; +using NutritionService.API.Repositories; + + +var builder = WebApplication.CreateBuilder(args); +var consulConfig = builder.Configuration.GetSection("ConsulConfig").Get()!; +builder.Services.AddSingleton(consulConfig); +builder.Services.AddSingleton(provider => new ConsulClient(config => +{ + config.Address = new Uri(consulConfig.Address); +})); + +builder.Services.AddSingleton(sp => +{ + var config = builder.Configuration.GetSection("DatabaseSettings"); + var client = new MongoClient(config["ConnectionString"]); + return client.GetDatabase(config["DatabaseName"]); +}); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddCors(o => +{ + o.AddPolicy("CorsPolicy", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); +}); + +builder.Services.AddSingleton(); + +var jwtSettings = builder.Configuration.GetSection("JwtSettings"); +var secretKey = jwtSettings.GetValue("secretKey"); + +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) + }; + }); + +var app = builder.Build(); + +app.Lifetime.ApplicationStarted.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + + var registration = new AgentServiceRegistration + { + ID = consulConfig.ServiceId, + Name = consulConfig.ServiceName, + Address = consulConfig.ServiceAddress, + Port = consulConfig.ServicePort + }; + + consulClient.Agent.ServiceRegister(registration).Wait(); +}); + +app.Lifetime.ApplicationStopping.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); +}); + +app.UseCors("CorsPolicy"); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); + + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/FoodRepository.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/FoodRepository.cs new file mode 100644 index 0000000..9a65540 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/FoodRepository.cs @@ -0,0 +1,32 @@ +using MongoDB.Driver; +using NutritionService.API.Models; + +namespace NutritionService.API.Repositories +{ + public class FoodRepository : IFoodRepository + { + private readonly IMongoCollection _foods; + + public FoodRepository(IConfiguration config) + { + var client = new MongoClient(config.GetValue("DatabaseSettings:ConnectionString")); + var database = client.GetDatabase(config.GetValue("DatabaseSettings:DatabaseName")); + _foods = database.GetCollection("Foods"); + } + + public async Task> GetAllAsync() => + await _foods.Find(f => true).ToListAsync(); + + public async Task GetByIdAsync(string id) => + await _foods.Find(f => f.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(Food food) => + await _foods.InsertOneAsync(food); + + public async Task UpdateAsync(Food food) => + await _foods.ReplaceOneAsync(f => f.Id == food.Id, food); + + public async Task DeleteAsync(string id) => + await _foods.DeleteOneAsync(f => f.Id == id); + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/IFoodRepository.cs b/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/IFoodRepository.cs new file mode 100644 index 0000000..47779aa --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/Repositories/IFoodRepository.cs @@ -0,0 +1,13 @@ +using NutritionService.API.Models; + +namespace NutritionService.API.Repositories +{ + public interface IFoodRepository + { + Task> GetAllAsync(); + Task GetByIdAsync(string id); + Task CreateAsync(Food food); + Task UpdateAsync(Food food); + Task DeleteAsync(string id); + } +} diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.Development.json b/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.Development.json new file mode 100644 index 0000000..6395a63 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.Development.json @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConsulConfig": { + "Address": "http://consul:8500", + "ServiceName": "NutritionService.API", + "ServiceId": "NutritionService.API-1", + "ServiceAddress": "nutritionservice.api", + "ServicePort": 8080 + }, + "DatabaseSettings": { + "ConnectionString": "mongodb://nutritiondb:27017", + "DatabaseName": "NutritionDb" + }, + "JwtSettings": { + "validIssuer": "Fitness Identity", + "validAudience": "Fitness", + "secretKey": "MyVeryVerySecretMessageForSecretKey", + "expires": 15 + } +} + diff --git a/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.json b/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.json new file mode 100644 index 0000000..5004041 --- /dev/null +++ b/Fitness/Backend/Services/NutritionService/NutritionService.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" + } + \ No newline at end of file diff --git a/Fitness/Backend/Services/PaymentService/PaymentService.API/PaymentService.API.csproj b/Fitness/Backend/Services/PaymentService/PaymentService.API/PaymentService.API.csproj index c2387a0..575d6c7 100644 --- a/Fitness/Backend/Services/PaymentService/PaymentService.API/PaymentService.API.csproj +++ b/Fitness/Backend/Services/PaymentService/PaymentService.API/PaymentService.API.csproj @@ -11,6 +11,7 @@ + diff --git a/Fitness/Backend/Services/PaymentService/PaymentService.API/Program.cs b/Fitness/Backend/Services/PaymentService/PaymentService.API/Program.cs index d4543aa..08645b7 100644 --- a/Fitness/Backend/Services/PaymentService/PaymentService.API/Program.cs +++ b/Fitness/Backend/Services/PaymentService/PaymentService.API/Program.cs @@ -11,6 +11,8 @@ // Add services to the container. +DotNetEnv.Env.Load(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(consulConfig); @@ -65,7 +67,7 @@ }); -// app.UseAuthorization(); +app.UseAuthorization(); app.MapControllers(); diff --git a/Fitness/Backend/Services/PaymentService/PaymentService.API/appsettings.Development.json b/Fitness/Backend/Services/PaymentService/PaymentService.API/appsettings.Development.json index 794be95..d40c87a 100644 --- a/Fitness/Backend/Services/PaymentService/PaymentService.API/appsettings.Development.json +++ b/Fitness/Backend/Services/PaymentService/PaymentService.API/appsettings.Development.json @@ -8,10 +8,6 @@ "DatabaseSettings": { "ConnectionString": "mongodb://localhost:27017" }, - "PayPalSettings": { - "ClientId": "AbO70ZPXZV0PKg39y4Fi5V3iv0evxuUweijLTuzDYewszFByKPWAmEktlnMVFsgN3g2txL_7v4KAx6M8", - "ClientSecret": "EJNTklAoNzMMAxNr5mB9ao5R6VyykKWv1TnMGySQZoYiH9aQuKbZzapOLzbFHRejZar5hO5BWQtVg7fz" - }, "ConsulConfig": { "Address": "http://consul:8500", "ServiceName": "PaymentService.API", diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Controllers/ReservationController.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Controllers/ReservationController.cs new file mode 100644 index 0000000..cf1ae97 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Controllers/ReservationController.cs @@ -0,0 +1,195 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ReservationService.API.Entities; +using ReservationService.API.Services; + +// ReSharper disable All + +namespace ReservationService.API.Controllers; + +[Authorize] +[ApiController] +[Route("api/v1/[controller]")] +public class ReservationController : ControllerBase +{ + private readonly IReservationService _reservationService; + + public ReservationController(IReservationService reservationService) + { + _reservationService = reservationService ?? throw new ArgumentNullException(nameof(reservationService)); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("individual")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetIndividualReservations() + { + var reservations = await _reservationService.GetIndividualReservationsAsync(); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("group")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetGroupReservations() + { + var reservations = await _reservationService.GetGroupReservationsAsync(); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("individual/{id}", Name="GetIndividualReservation")] + [ProducesResponseType(typeof(IndividualReservation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetIndividualReservation(string id) + { + var reservation = await _reservationService.GetIndividualReservationAsync(id); + if (reservation == null) return NotFound(); + return Ok(reservation); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("group/{id}", Name = "GetGroupReservation")] + [ProducesResponseType(typeof(GroupReservation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetGroupReservation(string id) + { + var reservation = await _reservationService.GetGroupReservationAsync(id); + if (reservation == null) return NotFound(); + return Ok(reservation); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("individual/client/{clientId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetIndividualReservationsByClientId(string clientId) + { + var reservations = await _reservationService.GetIndividualReservationsByClientIdAsync(clientId); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("group/client/{clientId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetGroupReservationsByClientId(string clientId) + { + var reservations = await _reservationService.GetGroupReservationsByClientIdAsync(clientId); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("individual/trainer/{trainerId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetIndividualReservationsByTrainerId(string trainerId) + { + var reservations = await _reservationService.GetIndividualReservationsByTrainerIdAsync(trainerId); + return Ok(reservations); + } + + [Authorize(Roles = "Admin, Client, Trainer")] + [HttpGet("group/trainer/{trainerId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetGroupReservationsByTrainerId(string trainerId) + { + var reservations = await _reservationService.GetGroupReservationsByTrainerIdAsync(trainerId); + return Ok(reservations); + } + + [Authorize(Roles = "Client")] + [HttpPost("individual")] + [ProducesResponseType(typeof(IndividualReservation), StatusCodes.Status201Created)] + public async Task> CreateIndividualReservation([FromBody] IndividualReservation reservation) + { + var created = await _reservationService.CreateIndividualReservationAsync(reservation); + if (created) + { + return CreatedAtRoute(nameof(GetIndividualReservation), new { id = reservation.Id }, reservation); + } + else + { + return BadRequest(); + } + } + + [Authorize(Roles = "Trainer")] + [HttpPost("group")] + [ProducesResponseType(typeof(GroupReservation), StatusCodes.Status201Created)] + public async Task> CreateGroupReservation([FromBody] GroupReservation reservation) + { + var created = await _reservationService.CreateGroupReservationAsync(reservation); + if (created) + { + return CreatedAtRoute(nameof(GetGroupReservation), new { id = reservation.Id }, reservation); + } + else + { + return BadRequest(); + } + } + + [Authorize(Roles = "Trainer")] + [HttpDelete("group/{id}")] + [ProducesResponseType(typeof(GroupReservation), StatusCodes.Status200OK)] + public async Task DeleteGroupReservation(string id) + { + var deleted = await _reservationService.DeleteGroupReservationAsync(id); + if (deleted) + { + return NoContent(); + } + else + { + return BadRequest(); + } + } + + [Authorize(Roles = "Client")] + [HttpPut("individual/client/cancel/{id}")] + [ProducesResponseType(typeof(IndividualReservation), StatusCodes.Status204NoContent)] + public async Task CancelClientIndividualReservation(string id) + { + var cancelled = await _reservationService.ClientCancelIndividualReservationAsync(id); + return cancelled ? Ok(cancelled) : BadRequest(); + } + + [Authorize(Roles = "Trainer")] + [HttpPut("individual/trainer/cancel/{id}")] + [ProducesResponseType(typeof(IndividualReservation), StatusCodes.Status204NoContent)] + public async Task CancelTrainerIndividualReservation(string id) + { + var cancelled = await _reservationService.TrainerCancelIndividualReservationAsync(id); + return cancelled ? Ok(cancelled) : BadRequest(); + } + + [Authorize(Roles = "Client")] + [HttpPost("group/book/{id}")] + [ProducesResponseType(typeof(GroupReservation), StatusCodes.Status200OK)] + public async Task BookGroupReservation(string id, [FromQuery] string clientId) + { + var booked = await _reservationService.BookGroupReservationAsync(id, clientId); + if (booked) + { + return Ok(); + } + else + { + return BadRequest(); + } + } + + [Authorize(Roles = "Client")] + [HttpPost("group/cancel/{id}")] + [ProducesResponseType(typeof(GroupReservation), StatusCodes.Status204NoContent)] + public async Task CancelGroupReservation(string id, [FromQuery] string clientId) + { + var cancelled = await _reservationService.CancelGroupReservationAsync(id, clientId); + if (cancelled) + { + return NoContent(); + } + else + { + return BadRequest(); + } + } +} diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/Context.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/Context.cs new file mode 100644 index 0000000..e95b383 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/Context.cs @@ -0,0 +1,20 @@ +using MongoDB.Driver; +using ReservationService.API.Entities; + +namespace ReservationService.API.Data; + +public class Context : IContext +{ + public IMongoCollection IndividualReservations { get; } + public IMongoCollection GroupReservations { get; } + + public Context(IConfiguration configuration) + { + var client = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString")); + var database = client.GetDatabase("Reservations"); + + IndividualReservations = database.GetCollection("IndividualReservations"); + GroupReservations = database.GetCollection("GroupReservations"); + } + +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/IContext.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/IContext.cs new file mode 100644 index 0000000..065d867 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Data/IContext.cs @@ -0,0 +1,10 @@ +using MongoDB.Driver; +using ReservationService.API.Entities; + +namespace ReservationService.API.Data; + +public interface IContext +{ + IMongoCollection IndividualReservations { get; } + IMongoCollection GroupReservations { get; } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Dockerfile b/Fitness/Backend/Services/ReservationService/ReservationService.API/Dockerfile new file mode 100644 index 0000000..a1a44c7 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Services/ReservationService/ReservationService.API/ReservationService.API.csproj", "Services/ReservationService/ReservationService.API/"] +RUN dotnet restore "Services/ReservationService/ReservationService.API/ReservationService.API.csproj" +COPY . . +WORKDIR "/src/Services/ReservationService/ReservationService.API" +RUN dotnet build "ReservationService.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "ReservationService.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ReservationService.API.dll"] diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservation.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservation.cs new file mode 100644 index 0000000..ddf6696 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservation.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace ReservationService.API.Entities; + +public class GroupReservation +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string Name { get; set; } + public string About { get; set; } + public string TrainerId { get; set; } + public int Capacity { get; set; } + public List ClientIds { get; set; } + public TimeOnly StartTime { get; set; } + public TimeOnly EndTime { get; set; } + public DateOnly Date { get; set; } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservationEvent.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservationEvent.cs new file mode 100644 index 0000000..24e37e6 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/GroupReservationEvent.cs @@ -0,0 +1,22 @@ +namespace ReservationService.API.Entities; + +public class GroupReservationEvent +{ + public string ReservationId { get; set; } + public string? ClientId { get; set; } + public string? TrainerId { get; set; } + public string? TrainingName { get; set; } + public int? Capacity { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public GroupReservationEventType EventType { get; set; } +} + +public enum GroupReservationEventType +{ + Added, + Removed, + ClientBooked, + ClientCancelled, +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservation.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservation.cs new file mode 100644 index 0000000..57b8175 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservation.cs @@ -0,0 +1,25 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace ReservationService.API.Entities; + +public class IndividualReservation +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string ClientId { get; set; } + public string TrainerId { get; set; } + public string TrainingTypeId { get; set; } + public TimeOnly StartTime { get; set; } + public TimeOnly EndTime { get; set; } + public DateOnly Date { get; set; } + public IndividualReservationStatus Status { get; set; } = IndividualReservationStatus.Active; +} + +public enum IndividualReservationStatus +{ + Active, + ClientCancelled, + TrainerCancelled +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservationEvent.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservationEvent.cs new file mode 100644 index 0000000..c00e660 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/IndividualReservationEvent.cs @@ -0,0 +1,20 @@ +namespace ReservationService.API.Entities; + +public class IndividualReservationEvent +{ + public string ReservationId { get; set; } + public string? ClientId { get; set; } + public string? TrainerId { get; set; } + public string? TrainingTypeId { get; set; } + public TimeOnly? StartTime { get; set; } + public TimeOnly? EndTime { get; set; } + public DateOnly? Date { get; set; } + public IndividualReservationEventType EventType { get; set; } +} + +public enum IndividualReservationEventType +{ + Booked, + CancelledByClient, + CancelledByTrainer, +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/Notification.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/Notification.cs new file mode 100644 index 0000000..2751d1c --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Entities/Notification.cs @@ -0,0 +1,19 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace ReservationService.API.Entities; + +public class Notification +{ + public IDictionary UserIdToUserType; + public string Title { get; set; } + public string Content { get; set; } + public NotificationType Type { get; set; } + public bool Email { get; set; } + + public enum NotificationType + { + Information, + Warning + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Program.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Program.cs new file mode 100644 index 0000000..d32de0e --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Program.cs @@ -0,0 +1,123 @@ +using Consul; +using ConsulConfig.Settings; +using System.Text; +using EventBus.Messages.Events; +using MassTransit; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using ReservationService.API.Data; +using ReservationService.API.Entities; +using ReservationService.API.Publishers; +using ReservationService.API.Repository; +using ReservationService.API.Services; +using IndividualReservationEvent = ReservationService.API.Entities.IndividualReservationEvent; +using IndividualReservationEventType = ReservationService.API.Entities.IndividualReservationEventType; +using GroupReservationEvent = ReservationService.API.Entities.GroupReservationEvent; +using GroupReservationEventType = ReservationService.API.Entities.GroupReservationEventType; + +var builder = WebApplication.CreateBuilder(args); + +var consulConfig = builder.Configuration.GetSection("ConsulConfig").Get()!; +builder.Services.AddSingleton(consulConfig); +builder.Services.AddSingleton(provider => new ConsulClient(config => +{ + config.Address = new Uri(consulConfig.Address); +})); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("CorsPolicy", builder => + builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); +}); + +builder.Services.AddAutoMapper(configuration => +{ + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); +}); + +builder.Services.AddMassTransit(config => +{ + config.UsingRabbitMq((_, cfg) => + { + cfg.Host(builder.Configuration["EventBusSettings:HostAddress"]); + }); +}); + +var jwtSettings = builder.Configuration.GetSection("JwtSettings"); +var secretKey = jwtSettings.GetValue("secretKey"); + +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) + }; + }); + +var app = builder.Build(); + +app.UseCors("CorsPolicy"); + +app.Lifetime.ApplicationStarted.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + + var registration = new AgentServiceRegistration + { + ID = consulConfig.ServiceId, + Name = consulConfig.ServiceName, + Address = consulConfig.ServiceAddress, + Port = consulConfig.ServicePort + }; + + consulClient.Agent.ServiceRegister(registration).Wait(); +}); + +app.Lifetime.ApplicationStopping.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); +}); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Properties/launchSettings.json b/Fitness/Backend/Services/ReservationService/ReservationService.API/Properties/launchSettings.json new file mode 100644 index 0000000..d83a849 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36095", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5055", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/GroupReservationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/GroupReservationPublisher.cs new file mode 100644 index 0000000..6b3ace0 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/GroupReservationPublisher.cs @@ -0,0 +1,70 @@ +using AutoMapper; +using MassTransit; +using ReservationService.API.Entities; + +namespace ReservationService.API.Publishers; + +public class GroupReservationPublisher : IGroupReservationPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly IMapper _mapper; + + public GroupReservationPublisher(IPublishEndpoint publishEndpoint, IMapper mapper) + { + _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public async Task PublishAdded(GroupReservation groupReservation) + { + var added = new GroupReservationEvent + { + ReservationId = groupReservation.Id, + TrainerId = groupReservation.TrainerId, + TrainingName = groupReservation.Name, + Capacity = groupReservation.Capacity, + StartTime = groupReservation.StartTime, + EndTime = groupReservation.EndTime, + Date = groupReservation.Date, + EventType = GroupReservationEventType.Added + }; + var eventMessage = _mapper.Map(added); + await _publishEndpoint.Publish(eventMessage); + } + + public async Task PublishRemoved(GroupReservation groupReservation) + { + var removed = new GroupReservationEvent + { + ReservationId = groupReservation.Id, + TrainerId = groupReservation.TrainerId, + EventType = GroupReservationEventType.Removed + }; + var eventMessage = _mapper.Map(removed); + await _publishEndpoint.Publish(eventMessage); + } + + public async Task PublishBooked(GroupReservation groupReservation, string clientId) + { + var booked = new GroupReservationEvent + { + ReservationId = groupReservation.Id, + ClientId = clientId, + EventType = GroupReservationEventType.ClientBooked + }; + var eventMessage = _mapper.Map(booked); + await _publishEndpoint.Publish(eventMessage); + } + + public async Task PublishCancelled(GroupReservation groupReservation, string clientId) + { + var cancelled = new GroupReservationEvent + { + ReservationId = groupReservation.Id, + ClientId = clientId, + EventType = GroupReservationEventType.ClientCancelled + }; + var eventMessage = _mapper.Map(cancelled); + await _publishEndpoint.Publish(eventMessage); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IGroupReservationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IGroupReservationPublisher.cs new file mode 100644 index 0000000..14b6519 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IGroupReservationPublisher.cs @@ -0,0 +1,14 @@ +using ReservationService.API.Entities; + +namespace ReservationService.API.Publishers; + +public interface IGroupReservationPublisher +{ + Task PublishAdded(GroupReservation groupReservation); + + Task PublishRemoved(GroupReservation groupReservation); + + Task PublishBooked(GroupReservation groupReservation, string clientId); + + Task PublishCancelled(GroupReservation groupReservation, string clientId); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IIndividualReservationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IIndividualReservationPublisher.cs new file mode 100644 index 0000000..891760c --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IIndividualReservationPublisher.cs @@ -0,0 +1,12 @@ +using ReservationService.API.Entities; + +namespace ReservationService.API.Publishers; + +public interface IIndividualReservationPublisher +{ + Task PublishBooked(IndividualReservation individualReservation); + + Task PublishClientCancelled(IndividualReservation individualReservation); + + Task PublishTrainerCancelled(IndividualReservation individualReservation); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/INotificationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/INotificationPublisher.cs new file mode 100644 index 0000000..293906a --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/INotificationPublisher.cs @@ -0,0 +1,8 @@ +using MassTransit; + +namespace ReservationService.API.Publishers; + +public interface INotificationPublisher +{ + Task PublishNotification(string title, string content, string type, bool email, IDictionary users); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IndividualReservationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IndividualReservationPublisher.cs new file mode 100644 index 0000000..5192451 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/IndividualReservationPublisher.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using MassTransit; +using ReservationService.API.Entities; + +namespace ReservationService.API.Publishers; + +public class IndividualReservationPublisher : IIndividualReservationPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly IMapper _mapper; + + public IndividualReservationPublisher(IPublishEndpoint publishEndpoint, IMapper mapper) + { + _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public Task PublishBooked(IndividualReservation individualReservation) + { + var booked = new IndividualReservationEvent + { + ReservationId = individualReservation.Id, + ClientId = individualReservation.ClientId, + TrainerId = individualReservation.TrainerId, + TrainingTypeId = individualReservation.TrainingTypeId, + StartTime = individualReservation.StartTime, + EndTime = individualReservation.EndTime, + Date = individualReservation.Date, + EventType = IndividualReservationEventType.Booked + }; + var eventMessage = _mapper.Map(booked); + return _publishEndpoint.Publish(eventMessage); + } + + public Task PublishClientCancelled(IndividualReservation individualReservation) + { + var cancelled = new IndividualReservationEvent + { + ReservationId = individualReservation.Id, + ClientId = individualReservation.ClientId, + EventType = IndividualReservationEventType.CancelledByClient + }; + var eventMessage = _mapper.Map(cancelled); + return _publishEndpoint.Publish(eventMessage); + } + + public Task PublishTrainerCancelled(IndividualReservation individualReservation) + { + var cancelled = new IndividualReservationEvent + { + ReservationId = individualReservation.Id, + TrainerId = individualReservation.TrainerId, + EventType = IndividualReservationEventType.CancelledByTrainer + }; + var eventMessage = _mapper.Map(cancelled); + return _publishEndpoint.Publish(eventMessage); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/NotificationPublisher.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/NotificationPublisher.cs new file mode 100644 index 0000000..b99678b --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Publishers/NotificationPublisher.cs @@ -0,0 +1,34 @@ +using AutoMapper; +using EventBus.Messages.Events; +using MassTransit; +using ReservationService.API.Entities; + +namespace ReservationService.API.Publishers; + +public class NotificationPublisher : INotificationPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly IMapper _mapper; + + public NotificationPublisher(IPublishEndpoint publishEndpoint, IMapper mapper) + { + _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public async Task PublishNotification(string title, string content, string type, bool email, IDictionary users) + { + Enum.TryParse(type, out Notification.NotificationType notificationType); + var notification = new Notification + { + Title = title, + Content = content, + Type = notificationType, + Email = email, + UserIdToUserType = users + }; + + var eventMessage = _mapper.Map(notification); + await _publishEndpoint.Publish(eventMessage); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/IReservationRepository.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/IReservationRepository.cs new file mode 100644 index 0000000..fedc3c6 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/IReservationRepository.cs @@ -0,0 +1,27 @@ +using ReservationService.API.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ReservationService.API.Repository +{ + public interface IReservationRepository + { + Task> GetIndividualReservationsAsync(); + Task> GetGroupReservationsAsync(); + Task GetIndividualReservationByIdAsync(string id); + Task GetGroupReservationByIdAsync(string id); + Task> GetIndividualReservationsByClientIdAsync(string clientId); + Task> GetGroupReservationsByClientIdAsync(string clientId); + Task> GetIndividualReservationsByTrainerIdAsync(string trainerId); + Task> GetGroupReservationsByTrainerIdAsync(string trainerId); + Task CreateIndividualReservationAsync(IndividualReservation reservation); + Task CreateGroupReservationAsync(GroupReservation reservation); + Task UpdateIndividualReservationAsync(IndividualReservation reservation); + Task UpdateGroupReservationAsync(GroupReservation reservation); + Task DeleteIndividualReservationAsync(string id); + Task DeleteGroupReservationAsync(string id); + Task BookGroupReservationAsync(string id, string clientId); + Task CancelGroupReservationAsync(string id, string clientId); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/ReservationRepository.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/ReservationRepository.cs new file mode 100644 index 0000000..2c411c0 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Repository/ReservationRepository.cs @@ -0,0 +1,112 @@ +using MongoDB.Driver; +using ReservationService.API.Data; +using ReservationService.API.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ReservationService.API.Repository +{ + public class ReservationRepository : IReservationRepository + { + private readonly IContext _context; + + public ReservationRepository(IContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task> GetIndividualReservationsAsync() + { + return await _context.IndividualReservations.Find(_ => true).ToListAsync(); + } + + public async Task> GetGroupReservationsAsync() + { + return await _context.GroupReservations.Find(_ => true).ToListAsync(); + } + + public async Task GetIndividualReservationByIdAsync(string id) + { + return await _context.IndividualReservations.Find(r => r.Id == id).FirstOrDefaultAsync(); + } + + public async Task GetGroupReservationByIdAsync(string id) + { + return await _context.GroupReservations.Find(r => r.Id == id).FirstOrDefaultAsync(); + } + + public async Task> GetIndividualReservationsByClientIdAsync(string clientId) + { + return await _context.IndividualReservations.Find(r => r.ClientId == clientId).ToListAsync(); + } + + public async Task> GetGroupReservationsByClientIdAsync(string clientId) + { + return await _context.GroupReservations.Find(r => r.ClientIds.Contains(clientId)).ToListAsync(); + } + + public async Task> GetIndividualReservationsByTrainerIdAsync(string trainerId) + { + return await _context.IndividualReservations.Find(r => r.TrainerId == trainerId).ToListAsync(); + } + + public async Task> GetGroupReservationsByTrainerIdAsync(string trainerId) + { + return await _context.GroupReservations.Find(r => r.TrainerId == trainerId).ToListAsync(); + } + + public async Task CreateIndividualReservationAsync(IndividualReservation reservation) + { + await _context.IndividualReservations.InsertOneAsync(reservation); + } + + public async Task CreateGroupReservationAsync(GroupReservation reservation) + { + await _context.GroupReservations.InsertOneAsync(reservation); + } + + public async Task UpdateIndividualReservationAsync(IndividualReservation reservation) + { + var result = await _context.IndividualReservations.ReplaceOneAsync(r => r.Id == reservation.Id, reservation); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task UpdateGroupReservationAsync(GroupReservation reservation) + { + var result = await _context.GroupReservations.ReplaceOneAsync(r => r.Id == reservation.Id, reservation); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task DeleteIndividualReservationAsync(string id) + { + var result = await _context.IndividualReservations.DeleteOneAsync(r => r.Id == id); + return result.IsAcknowledged && result.DeletedCount > 0; + } + + public async Task DeleteGroupReservationAsync(string id) + { + var result = await _context.GroupReservations.DeleteOneAsync(r => r.Id == id); + return result.IsAcknowledged && result.DeletedCount > 0; + } + + public async Task BookGroupReservationAsync(string id, string clientId) + { + var reservation = await GetGroupReservationByIdAsync(id); + if (reservation.Capacity <= reservation.ClientIds.Count || reservation.ClientIds.Contains(clientId)) + return false; + + reservation.ClientIds.Add(clientId); + var result = await _context.GroupReservations.ReplaceOneAsync(r => r.Id == id, reservation); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task CancelGroupReservationAsync(string id, string clientId) + { + var reservation = await GetGroupReservationByIdAsync(id); + reservation.ClientIds.Remove(clientId); + var result = await _context.GroupReservations.ReplaceOneAsync(r => r.Id == id, reservation); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.csproj b/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.csproj new file mode 100644 index 0000000..c54172a --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + + + + + + + + + .dockerignore + + + + + + + + + diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.http b/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.http new file mode 100644 index 0000000..f6e5109 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/ReservationService.API.http @@ -0,0 +1,6 @@ +@ReservationService.API_HostAddress = http://localhost:5055 + +GET {{ReservationService.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/IReservationService.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/IReservationService.cs new file mode 100644 index 0000000..3836efa --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/IReservationService.cs @@ -0,0 +1,36 @@ +using ReservationService.API.Entities; + +namespace ReservationService.API.Services; + +public interface IReservationService +{ + Task> GetIndividualReservationsAsync(); + + Task> GetGroupReservationsAsync(); + + Task GetIndividualReservationAsync(string id); + + Task GetGroupReservationAsync(string id); + + Task> GetIndividualReservationsByClientIdAsync(string clientId); + + Task> GetGroupReservationsByClientIdAsync(string clientId); + + Task> GetIndividualReservationsByTrainerIdAsync(string trainerId); + + Task> GetGroupReservationsByTrainerIdAsync(string trainerId); + + Task CreateIndividualReservationAsync(IndividualReservation individualReservation); + + Task CreateGroupReservationAsync(GroupReservation groupReservation); + + Task DeleteGroupReservationAsync(string id); + + Task BookGroupReservationAsync(string id, string clientId); + + Task CancelGroupReservationAsync(string id, string clientId); + + Task ClientCancelIndividualReservationAsync(string id); + + Task TrainerCancelIndividualReservationAsync(string id); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/ReservationService.cs b/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/ReservationService.cs new file mode 100644 index 0000000..702f379 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/Services/ReservationService.cs @@ -0,0 +1,280 @@ +using MassTransit; +using ReservationService.API.Entities; +using ReservationService.API.Publishers; +using ReservationService.API.Repository; + +namespace ReservationService.API.Services; + +public class ReservationService : IReservationService +{ + private readonly IReservationRepository _reservationRepository; + private readonly INotificationPublisher _notificationPublisher; + private readonly IIndividualReservationPublisher _individualReservationPublisher; + private readonly IGroupReservationPublisher _groupReservationPublisher; + public ReservationService(IReservationRepository reservationRepository, INotificationPublisher notificationPublisher, + IIndividualReservationPublisher individualReservationPublisher, IGroupReservationPublisher groupReservationPublisher) + { + _reservationRepository = reservationRepository ?? throw new ArgumentNullException(nameof(reservationRepository)); + _notificationPublisher = notificationPublisher ?? throw new ArgumentNullException(nameof(notificationPublisher)); + _individualReservationPublisher = individualReservationPublisher ?? throw new ArgumentNullException(nameof(individualReservationPublisher)); + _groupReservationPublisher = groupReservationPublisher ?? throw new ArgumentNullException(nameof(groupReservationPublisher)); + } + + public async Task> GetIndividualReservationsAsync() + { + var reservations = await _reservationRepository.GetIndividualReservationsAsync(); + return reservations; + } + + public async Task> GetGroupReservationsAsync() + { + var reservations = await _reservationRepository.GetGroupReservationsAsync(); + return reservations; + } + + public async Task GetIndividualReservationAsync(string id) + { + var reservation = await _reservationRepository.GetIndividualReservationByIdAsync(id); + return reservation; + } + + public async Task GetGroupReservationAsync(string id) + { + var reservation = await _reservationRepository.GetGroupReservationByIdAsync(id); + return reservation; + } + + public async Task> GetIndividualReservationsByClientIdAsync(string clientId) + { + var reservations = await _reservationRepository.GetIndividualReservationsByClientIdAsync(clientId); + return reservations; + } + + public async Task> GetGroupReservationsByClientIdAsync(string clientId) + { + var reservations = await _reservationRepository.GetGroupReservationsByClientIdAsync(clientId); + return reservations; + } + + public async Task> GetIndividualReservationsByTrainerIdAsync(string trainerId) + { + var reservations = await _reservationRepository.GetIndividualReservationsByTrainerIdAsync(trainerId); + return reservations; + } + + public async Task> GetGroupReservationsByTrainerIdAsync(string trainerId) + { + var reservations = await _reservationRepository.GetGroupReservationsByTrainerIdAsync(trainerId); + return reservations; + } + + public async Task CreateIndividualReservationAsync(IndividualReservation individualReservation) + { + if (await IsClientFree(individualReservation.ClientId, individualReservation.Date, + individualReservation.StartTime, individualReservation.EndTime) + && await IsTrainerFree(individualReservation.TrainerId, individualReservation.Date, + individualReservation.StartTime, individualReservation.EndTime)) + { + await _reservationRepository.CreateIndividualReservationAsync(individualReservation); + var users = new Dictionary + { + { individualReservation.ClientId, "Client" }, + { individualReservation.TrainerId, "Trainer" } + }; + await _notificationPublisher.PublishNotification("Training reservation created", individualReservation.ToString(), "Information", true, users); + await _individualReservationPublisher.PublishBooked(individualReservation); + + return true; + } + + return false; + } + + public async Task CreateGroupReservationAsync(GroupReservation groupReservation) + { + if (await IsTrainerFree(groupReservation.TrainerId, groupReservation.Date, + groupReservation.StartTime, groupReservation.EndTime)) + { + await _reservationRepository.CreateGroupReservationAsync(groupReservation); + var users = new Dictionary { { groupReservation.TrainerId, "Trainer" } }; + await _notificationPublisher.PublishNotification("Training reservation created", groupReservation.ToString(), "Information", true, users); + await _groupReservationPublisher.PublishAdded(groupReservation); + + return true; + } + + return false; + } + + public async Task DeleteGroupReservationAsync(string id) + { + var reservation = await _reservationRepository.GetGroupReservationByIdAsync(id); + var deleted = await _reservationRepository.DeleteGroupReservationAsync(id); + + if (deleted) + { + var users = new Dictionary(); + foreach (var clientId in reservation.ClientIds) + { + users.Add(clientId, "Client"); + } + users.Add(reservation.TrainerId, "Trainer"); + await _notificationPublisher.PublishNotification("Training reservation cancelled", reservation.ToString(), + "Information", true, users); + await _groupReservationPublisher.PublishRemoved(reservation); + } + + return deleted; + } + + public async Task BookGroupReservationAsync(string id, string clientId) + { + var groupReservation = await _reservationRepository.GetGroupReservationByIdAsync(id); + if (await IsClientFree(clientId, groupReservation.Date, groupReservation.StartTime, groupReservation.EndTime)) + { + var booked = await _reservationRepository.BookGroupReservationAsync(id, clientId); + if (booked) + { + var users = new Dictionary + { + { clientId, "Client" }, + { groupReservation.TrainerId, "Trainer" } + }; + await _notificationPublisher.PublishNotification("Training reservation booked", groupReservation.ToString(), + "Information", true, users); + await _groupReservationPublisher.PublishBooked(groupReservation, clientId); + } + + return booked; + } + + return false; + } + + public async Task CancelGroupReservationAsync(string id, string clientId) + { + var groupReservation = await _reservationRepository.GetGroupReservationByIdAsync(id); + var cancelled = await _reservationRepository.CancelGroupReservationAsync(id, clientId); + + if (cancelled) + { + var users = new Dictionary + { + { clientId, "Client" }, + { groupReservation.TrainerId, "Trainer" } + }; + await _notificationPublisher.PublishNotification("Training reservation cancelled", groupReservation.ToString(), + "Information", true, users); + await _groupReservationPublisher.PublishCancelled(groupReservation, clientId); + } + + return cancelled; + } + + public async Task ClientCancelIndividualReservationAsync(string id) + { + var reservation = await _reservationRepository.GetIndividualReservationByIdAsync(id); + reservation.Status = IndividualReservationStatus.ClientCancelled; + var cancelled = await _reservationRepository.UpdateIndividualReservationAsync(reservation); + + if (cancelled) + { + var users = new Dictionary + { + { reservation.ClientId, "Client" }, + { reservation.TrainerId, "Trainer" } + }; + await _notificationPublisher.PublishNotification("Training reservation cancelled by client", reservation.ToString(), + "Information", true, users); + await _individualReservationPublisher.PublishClientCancelled(reservation); + } + + return cancelled; + } + + public async Task TrainerCancelIndividualReservationAsync(string id) + { + var reservation = await _reservationRepository.GetIndividualReservationByIdAsync(id); + reservation.Status = IndividualReservationStatus.TrainerCancelled; + var cancelled = await _reservationRepository.UpdateIndividualReservationAsync(reservation); + + if (cancelled) + { + var users = new Dictionary + { + { reservation.ClientId, "Client" }, + { reservation.TrainerId, "Trainer" } + }; + await _notificationPublisher.PublishNotification("Training reservation cancelled by trainer", reservation.ToString(), + "Information", true, users); + await _individualReservationPublisher.PublishTrainerCancelled(reservation); + } + + return cancelled; + } + + private async Task IsClientFree(string clientId, DateOnly date, TimeOnly start, TimeOnly end) + { + var individualReservations = await _reservationRepository.GetIndividualReservationsByClientIdAsync(clientId); + var groupReservations = await _reservationRepository.GetGroupReservationsByClientIdAsync(clientId); + + foreach (var individualReservation in individualReservations) + { + if (individualReservation.Status == IndividualReservationStatus.Active && individualReservation.Date == date && IntervalsOverlap(individualReservation.StartTime, + individualReservation.EndTime, start, end)) + { + return false; + } + } + + foreach (var groupReservation in groupReservations) + { + if (groupReservation.Date == date && IntervalsOverlap(groupReservation.StartTime, + groupReservation.EndTime, start, end)) + { + return false; + } + } + + return true; + } + + private async Task IsTrainerFree(string trainerId, DateOnly date, TimeOnly start, TimeOnly end) + { + var individualReservations = await _reservationRepository.GetIndividualReservationsByTrainerIdAsync(trainerId); + var groupReservations = await _reservationRepository.GetGroupReservationsByTrainerIdAsync(trainerId); + + foreach (var individualReservation in individualReservations) + { + if (individualReservation.Status == IndividualReservationStatus.Active && individualReservation.Date == date && IntervalsOverlap(individualReservation.StartTime, + individualReservation.EndTime, start, end)) + { + return false; + } + } + + foreach (var groupReservation in groupReservations) + { + if (groupReservation.Date == date && IntervalsOverlap(groupReservation.StartTime, + groupReservation.EndTime, start, end)) + { + return false; + } + } + + return true; + } + + private bool IntervalsOverlap(TimeOnly start1, TimeOnly end1, TimeOnly start2, TimeOnly end2) + { + var s1 = start1.ToTimeSpan(); + var e1 = end1.ToTimeSpan(); + var s2 = start2.ToTimeSpan(); + var e2 = end2.ToTimeSpan(); + + if (e1 <= s1) e1 = e1.Add(TimeSpan.FromDays(1)); + if (e2 <= s2) e2 = e2.Add(TimeSpan.FromDays(1)); + + return s1 < e2 && s2 < e1; + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.Development.json b/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.Development.json new file mode 100644 index 0000000..ec459ef --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "JwtSettings": { + "validIssuer": "Fitness Identity", + "validAudience": "Fitness", + "secretKey": "MyVeryVerySecretMessageForSecretKey", + "expires": 15 + }, + "ConsulConfig": { + "Address": "http://consul:8500", + "ServiceName": "ReservationService.API", + "ServiceId": "ReservationService.API-1", + "ServiceAddress": "reservationservice.api", + "ServicePort": 8080 + } +} diff --git a/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.json b/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Fitness/Backend/Services/ReservationService/ReservationService.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/Controllers/ReviewController.cs b/Fitness/Backend/Services/ReviewService/ReviewService.API/Controllers/ReviewController.cs index 0db9c05..7479b16 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.API/Controllers/ReviewController.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/Controllers/ReviewController.cs @@ -1,61 +1,88 @@ -using Microsoft.AspNetCore.Authorization; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using ReviewService.API.Publishers; using ReviewService.Common.DTOs; using ReviewService.Common.Repositories; namespace ReviewService.API.Controllers { - // [Authorize] + [Authorize] [ApiController] [Route("api/v1/[controller]")] public class ReviewController : ControllerBase { private readonly IReviewRepository _repository; + private readonly IReviewPublisher _reviewPublisher; - public ReviewController(IReviewRepository repository) + public ReviewController(IReviewRepository repository, IReviewPublisher reviewPublisher) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _reviewPublisher = reviewPublisher ?? throw new ArgumentNullException(nameof(reviewPublisher)); } - // [Authorize(Roles = "Admin, Trainer, Client")] - [HttpGet("{trainerId}", Name = "GetReviews")] + [Authorize(Roles = "Admin, Trainer")] + [HttpGet("trainer/{trainerId}", Name = "GetReviewsByTrainerId")] [ProducesResponseType(typeof(ReviewDTO), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] - public async Task>> GetReviews(string trainerId) + public async Task>> GetReviewsByTrainerId(string trainerId) { - var reviews = await _repository.GetReviews(trainerId); - if (reviews == null || !reviews.Any()) + var reviews = await _repository.GetReviewsByTrainerId(trainerId); + if (!reviews.Any()) { return NotFound(); } return Ok(reviews); } - - // [Authorize(Roles = "Client")] - [HttpPost] - [ProducesResponseType(typeof(ReviewDTO), StatusCodes.Status201Created)] - public async Task> CreateReview([FromBody] CreateReviewDTO reviewDTO) + + [Authorize(Roles = "Admin, Client")] + [HttpGet("client/{clientId}", Name = "GetReviewsByClientId")] + [ProducesResponseType(typeof(ReviewDTO), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] + public async Task>> GetReviewsByClientId(string clientId) { - await _repository.CreateReview(reviewDTO); - var reviews = await _repository.GetReviews(reviewDTO.TrainerId); - var newReview = reviews.FirstOrDefault(r => r.Comment == reviewDTO.Comment); - return CreatedAtRoute("GetReviews", new { trainerId = reviewDTO.TrainerId }, newReview); + var reviews = await _repository.GetReviewsByClientId(clientId); + if (!reviews.Any()) + { + return NotFound(); + } + return Ok(reviews); } - - // [Authorize(Roles = "Client")] - [HttpPut] - [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] - public async Task> UpdateReview([FromBody] UpdateReviewDTO review) + + [Authorize(Roles = "Trainer")] + [HttpPost("trainer/{trainerId}")] + [ProducesResponseType(typeof(ReviewDTO), StatusCodes.Status201Created)] + public async Task> TrainerReview(string trainerId, [FromBody] SubmitReviewDTO reviewDTO) { - return Ok(await _repository.UpdateReview(review)); + var review = await _repository.GetReviewByReservationId(reviewDTO.ReservationId); + if (review == null) + { + await _repository.CreateReview(reviewDTO.ReservationId, reviewDTO.ClientId, reviewDTO.TrainerId); + } + var updated = await _repository.SubmitTrainerReview(reviewDTO); + if (updated) + { + await _reviewPublisher.PublishReview(reviewDTO, false); + } + return updated ? Ok(review) : BadRequest(); } - - // [Authorize(Roles = "Client")] - [HttpDelete("{reviewId}", Name = "DeleteReview")] - [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] - public async Task> DeleteReview(string reviewId) + + [Authorize(Roles = "Client")] + [HttpPost("client/{clientId}")] + [ProducesResponseType(typeof(ReviewDTO), StatusCodes.Status201Created)] + public async Task> ClientReview(string clientId, [FromBody] SubmitReviewDTO reviewDTO) { - return Ok(await _repository.DeleteReview(reviewId)); + var review = await _repository.GetReviewByReservationIdClientId(reviewDTO.ReservationId, reviewDTO.ClientId); + if (review == null) + { + await _repository.CreateReview(reviewDTO.ReservationId, reviewDTO.ClientId, reviewDTO.TrainerId); + } + var updated = await _repository.SubmitClientReview(reviewDTO); + if (updated) + { + await _reviewPublisher.PublishReview(reviewDTO, true); + } + return updated ? Ok(review) : BadRequest(); } } -} +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/Events/ReviewEvent.cs b/Fitness/Backend/Services/ReviewService/ReviewService.API/Events/ReviewEvent.cs new file mode 100644 index 0000000..df25854 --- /dev/null +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/Events/ReviewEvent.cs @@ -0,0 +1,16 @@ +namespace ReviewService.Common.Entities; + +public class ReviewEvent +{ + public string ReservationId { get; set; } + public string? UserId { get; set; } + public string? Comment { get; set; } + public int? Rating { get; set; } + public ReviewEventType EventType { get; set; } +} + +public enum ReviewEventType +{ + ClientReview, + TrainerReview +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/Program.cs b/Fitness/Backend/Services/ReviewService/ReviewService.API/Program.cs index ec81a9f..b773ebe 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.API/Program.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/Program.cs @@ -1,9 +1,13 @@ +using MassTransit; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using ReviewService.Common.Extensions; using System.Text; using Consul; using ConsulConfig.Settings; +using ReviewService.API.Publishers; +using ReviewService.Common.DTOs; +using ReviewService.Common.Entities; using ReviewService.Common.Extensions; var builder = WebApplication.CreateBuilder(args); @@ -22,9 +26,15 @@ { config.Address = new Uri(consulConfig.Address); })); +builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddReviewCommonExtensions(); +builder.Services.AddAutoMapper(configuration => +{ + configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); +}); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -52,6 +62,13 @@ }; }); +builder.Services.AddMassTransit(config => +{ + config.UsingRabbitMq((_, cfg) => + { + cfg.Host(builder.Configuration["EventBusSettings:HostAddress"]); + }); +}); var app = builder.Build(); @@ -88,8 +105,8 @@ app.UseRouting(); -// app.UseAuthentication(); -// app.UseAuthorization(); +app.UseAuthentication(); +app.UseAuthorization(); app.MapControllers(); diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/IReviewPublisher.cs b/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/IReviewPublisher.cs new file mode 100644 index 0000000..cb17358 --- /dev/null +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/IReviewPublisher.cs @@ -0,0 +1,8 @@ +using ReviewService.Common.DTOs; + +namespace ReviewService.API.Publishers; + +public interface IReviewPublisher +{ + Task PublishReview(SubmitReviewDTO submittedReview, bool client); +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/ReviewPublisher.cs b/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/ReviewPublisher.cs new file mode 100644 index 0000000..dfa9a9b --- /dev/null +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/Publishers/ReviewPublisher.cs @@ -0,0 +1,32 @@ +using AutoMapper; +using MassTransit; +using ReviewService.Common.DTOs; +using ReviewService.Common.Entities; + +namespace ReviewService.API.Publishers; + +public class ReviewPublisher : IReviewPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly IMapper _mapper; + + public ReviewPublisher(IPublishEndpoint publishEndpoint, IMapper mapper) + { + _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + + public Task PublishReview(SubmitReviewDTO submittedReview, bool client) + { + var review = new ReviewEvent + { + ReservationId = submittedReview.ReservationId, + UserId = client ? submittedReview.ClientId : submittedReview.TrainerId, + Comment = client ? submittedReview.ClientComment : submittedReview.TrainerComment, + Rating = client ? submittedReview.ClientRating : submittedReview.TrainerRating, + EventType = client ? ReviewEventType.ClientReview : ReviewEventType.TrainerReview + }; + var eventMessage = _mapper.Map(review); + return _publishEndpoint.Publish(eventMessage); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.API/ReviewService.API.csproj b/Fitness/Backend/Services/ReviewService/ReviewService.API/ReviewService.API.csproj index a4bbbe7..b7e3e59 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.API/ReviewService.API.csproj +++ b/Fitness/Backend/Services/ReviewService/ReviewService.API/ReviewService.API.csproj @@ -11,6 +11,9 @@ + + + @@ -19,6 +22,7 @@ + diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/BaseReviewDTO.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/BaseReviewDTO.cs index 3c871a7..02eff67 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/BaseReviewDTO.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/BaseReviewDTO.cs @@ -8,9 +8,12 @@ namespace ReviewService.Common.DTOs { public class BaseReviewDTO { + public string ReservationId { get; set; } public string TrainerId { get; set; } - public string ClientId { get; set; } - public string Comment { get; set; } - public int Rating { get; set; } + public string? TrainerComment { get; set; } + public int? TrainerRating { get; set; } + public string? ClientId { get; set; } + public string? ClientComment { get; set; } + public int? ClientRating { get; set; } } } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/CreateReviewDTO.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/SubmitReviewDTO.cs similarity index 77% rename from Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/CreateReviewDTO.cs rename to Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/SubmitReviewDTO.cs index 437ce90..6bdd30c 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/CreateReviewDTO.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/SubmitReviewDTO.cs @@ -6,7 +6,7 @@ namespace ReviewService.Common.DTOs { - public class CreateReviewDTO : BaseReviewDTO + public class SubmitReviewDTO : BaseReviewDTO { } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/UpdateReviewDTO.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/UpdateReviewDTO.cs deleted file mode 100644 index b61caf1..0000000 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/DTOs/UpdateReviewDTO.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace ReviewService.Common.DTOs -{ - public class UpdateReviewDTO : BaseReviewIdentityDTO - { - } -} diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Entities/Review.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Entities/Review.cs index 08e6726..101ec3c 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Entities/Review.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Entities/Review.cs @@ -13,9 +13,12 @@ public class Review [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id; + public string ReservationId { get; set; } public string TrainerId { get; set; } + public string? TrainerComment { get; set; } + public int? TrainerRating { get; set; } public string ClientId { get; set; } - public string Comment { get; set; } - public int Rating { get; set; } + public string? ClientComment { get; set; } + public int? ClientRating { get; set; } } } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Extensions/ReviewCommonExtensions.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Extensions/ReviewCommonExtensions.cs index 7d1fd1d..2249529 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Extensions/ReviewCommonExtensions.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Extensions/ReviewCommonExtensions.cs @@ -16,8 +16,7 @@ public static void AddReviewCommonExtensions(this IServiceCollection services) services.AddAutoMapper(configuration => { configuration.CreateMap().ReverseMap(); - configuration.CreateMap().ReverseMap(); - configuration.CreateMap().ReverseMap(); + configuration.CreateMap().ReverseMap(); }); } } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/IReviewRepository.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/IReviewRepository.cs index 27e2228..03e5e94 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/IReviewRepository.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/IReviewRepository.cs @@ -9,9 +9,12 @@ namespace ReviewService.Common.Repositories { public interface IReviewRepository { - Task> GetReviews(string trainerId); - Task CreateReview(CreateReviewDTO review); - Task UpdateReview(UpdateReviewDTO review); - Task DeleteReview(string reviewId); + Task GetReviewByReservationId(string reservationId); + Task GetReviewByReservationIdClientId(string reservationId, string clientId); + Task> GetReviewsByTrainerId(string trainerId); + Task> GetReviewsByClientId(string clientId); + Task CreateReview(string reservationId, string clientId, string trainerId); + Task SubmitClientReview(SubmitReviewDTO clientReview); + Task SubmitTrainerReview(SubmitReviewDTO trainerReview); } } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/ReviewRepository.cs b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/ReviewRepository.cs index ce1ced9..d9676cf 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/ReviewRepository.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.Common/Repositories/ReviewRepository.cs @@ -3,13 +3,6 @@ using ReviewService.Common.Data; using ReviewService.Common.DTOs; using ReviewService.Common.Entities; -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; namespace ReviewService.Common.Repositories { @@ -24,32 +17,79 @@ public ReviewRepository(IReviewContext context, IMapper mapper) _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } - public async Task> GetReviews(string trainerId) + public async Task GetReviewByReservationId(string reservationId) { - var reviews = await _context.Reviews.Find(r => r.TrainerId == trainerId).ToListAsync(); - return _mapper.Map>(reviews); + var review = await _context.Reviews.Find(r => r.ReservationId == reservationId).FirstOrDefaultAsync(); + return _mapper.Map(review); } - public async Task CreateReview(CreateReviewDTO review) + public async Task GetReviewByReservationIdClientId(string reservationId, string clientId) { - var reviewEntity = _mapper.Map(review); - await _context.Reviews.InsertOneAsync(reviewEntity); + var review = await _context.Reviews + .Find(r => r.ReservationId == reservationId && r.ClientId == clientId) + .FirstOrDefaultAsync(); + return _mapper.Map(review); } - public async Task DeleteReview(string reviewId) + public async Task> GetReviewsByTrainerId(string trainerId) { - var deleteResult = await _context.Reviews.DeleteManyAsync(r => r.Id == reviewId); - return deleteResult.IsAcknowledged && deleteResult.DeletedCount > 0; + var reviews = await _context.Reviews + .Find(r => r.TrainerId == trainerId && r.ClientComment != null && r.ClientRating != null) + .ToListAsync(); + return _mapper.Map>(reviews); + } + public async Task> GetReviewsByClientId(string clientId) + { + var reviews = await _context.Reviews + .Find(r => r.ClientId == clientId && r.TrainerComment != null && r.TrainerRating != null) + .ToListAsync(); + return _mapper.Map>(reviews); } - public async Task UpdateReview(UpdateReviewDTO review) + public async Task CreateReview(string reservationId, string clientId, string trainerId) { - var reviewEntity = _mapper.Map(review); + var review = new ReviewDTO { ReservationId = reservationId, ClientId = clientId, TrainerId = trainerId}; + await _context.Reviews.InsertOneAsync(_mapper.Map(review)); + } + public async Task SubmitClientReview(SubmitReviewDTO clientReview) + { + var existingTrainerReview = await _context.Reviews + .Find(r => r.ReservationId == clientReview.ReservationId && r.TrainerComment != null && r.TrainerRating != null) + .FirstOrDefaultAsync(); + + var review = await GetReviewByReservationIdClientId(clientReview.ReservationId, clientReview.ClientId); + if (review == null) + { + throw new InvalidOperationException("Review not found."); + } + review.ClientComment = clientReview.ClientComment; + review.ClientRating = clientReview.ClientRating; + review.TrainerComment = existingTrainerReview?.TrainerComment; + review.TrainerRating = existingTrainerReview?.TrainerRating; + + var result = await _context.Reviews.ReplaceOneAsync(r => r.Id == review.Id, _mapper.Map(review)); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task SubmitTrainerReview(SubmitReviewDTO trainerReview) + { + var reviews = await _context.Reviews.Find(r => r.ReservationId == trainerReview.ReservationId).ToListAsync(); + if (reviews.Count == 0) + { + throw new InvalidOperationException("Review not found."); + } - var updateResult = await _context.Reviews.ReplaceOneAsync(p => p.Id == reviewEntity.Id, reviewEntity); - return updateResult.IsAcknowledged && updateResult.ModifiedCount > 0; + var result = true; + foreach (Review review in reviews) + { + review.TrainerComment = trainerReview.TrainerComment; + review.TrainerRating = trainerReview.TrainerRating; + var reviewResult = await _context.Reviews.ReplaceOneAsync(r => r.Id == review.Id, review); + result = result && reviewResult.IsAcknowledged && reviewResult.ModifiedCount > 0; + } + return result; } } } diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Protos/review.proto b/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Protos/review.proto index 8db8f1b..005579a 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Protos/review.proto +++ b/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Protos/review.proto @@ -15,8 +15,8 @@ message GetReviewsResponse { string id = 1; string trainerId = 2; string clientId = 3; - string comment = 4; - int32 rating = 5; + string clientComment = 4; + int32 clientRating = 5; } repeated ReviewReply reviews = 1; diff --git a/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Services/ReviewServiceGrpc.cs b/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Services/ReviewServiceGrpc.cs index cb1feb6..f78c073 100644 --- a/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Services/ReviewServiceGrpc.cs +++ b/Fitness/Backend/Services/ReviewService/ReviewService.GRPC/Services/ReviewServiceGrpc.cs @@ -21,7 +21,7 @@ public ReviewServiceGrpc(IReviewRepository repository, IMapper mapper) public override async Task GetReviews(GetReviewsRequest request, ServerCallContext context) { - var reviews = await _repository.GetReviews(request.TrainerId); + var reviews = await _repository.GetReviewsByTrainerId(request.TrainerId); var reviewList = new GetReviewsResponse(); reviewList.Reviews.AddRange(_mapper.Map>(reviews)); return reviewList; diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.API/Controllers/TrainerController.cs b/Fitness/Backend/Services/TrainerService/TrainerService.API/Controllers/TrainerController.cs index ff5788d..7428f50 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.API/Controllers/TrainerController.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.API/Controllers/TrainerController.cs @@ -10,7 +10,7 @@ namespace TrainerService.API.Controllers { - // [Authorize] + [Authorize] [ApiController] [Route("api/v1/[controller]")] public class TrainerController : ControllerBase @@ -18,20 +18,18 @@ public class TrainerController : ControllerBase private readonly ITrainerRepository _repository; private readonly ReviewGrpcService _reviewGrpcService; private readonly IMapper _mapper; - private readonly IPublishEndpoint _publishEndpoint; - public TrainerController(ITrainerRepository repository, ReviewGrpcService reviewGrpcService, IMapper mapper, IPublishEndpoint publishEndpoint) + public TrainerController(ITrainerRepository repository, ReviewGrpcService reviewGrpcService, IMapper mapper) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _reviewGrpcService = reviewGrpcService ?? throw new ArgumentNullException(nameof(reviewGrpcService)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint)); } - // [Authorize(Roles = "Admin, Client")] + [Authorize(Roles = "Admin, Trainer, Client")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetTrainers() @@ -46,7 +44,7 @@ public async Task>> GetTrainers() return Ok(trainers); } - // [Authorize(Roles = "Admin, Client, Trainer")] + [Authorize(Roles = "Admin, Client, Trainer")] [HttpGet("{id}", Name = "GetTrainer")] [ProducesResponseType(typeof(Trainer), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Trainer), StatusCodes.Status404NotFound)] @@ -65,7 +63,7 @@ public async Task> GetTrainerById(string id) } } - // [Authorize(Roles = "Admin, Client, Trainer")] + [Authorize(Roles = "Admin, Client, Trainer")] [HttpGet("[action]/{email}", Name = "GetTrainerByEmail")] [ProducesResponseType(typeof(Trainer), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Trainer), StatusCodes.Status404NotFound)] @@ -84,7 +82,7 @@ public async Task> GetTrainerByEmail(string email) } } - // [Authorize(Roles = "Admin, Client")] + [Authorize(Roles = "Admin, Client")] [Route("[action]/{minRating}")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] @@ -101,7 +99,7 @@ public async Task>> GetTrainersByRating(double return Ok(filteredTrainers); } - // [Authorize(Roles = "Admin, Client")] + [Authorize(Roles = "Admin, Client")] [Route("[action]/{trainingType}")] [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] @@ -117,7 +115,7 @@ public async Task>> GetTrainersByTrainingType( return Ok(trainers); } - // [Authorize(Roles = "Admin, Client, Trainer")] + [Authorize(Roles = "Admin, Client, Trainer")] [Route("[action]/{trainerId}/{trainingType}")] [HttpGet] [ProducesResponseType(typeof(decimal), StatusCodes.Status200OK)] @@ -128,7 +126,7 @@ public async Task> GetPrice(string trainerId, string train return Ok(price); } - // [Authorize(Roles = "Admin")] + [Authorize(Roles = "Admin")] [HttpPost] [ProducesResponseType(typeof(Trainer), StatusCodes.Status201Created)] public async Task> CreateTrainer([FromBody] Trainer trainer) @@ -148,7 +146,7 @@ public async Task> CreateTrainer([FromBody] Trainer traine return CreatedAtRoute("GetTrainer", new { id = trainer.Id }, trainer); } - // [Authorize(Roles = "Admin, Trainer")] + [Authorize(Roles = "Admin, Trainer")] [HttpPut] [ProducesResponseType(typeof(Trainer), StatusCodes.Status200OK)] public async Task UpdateTrainer([FromBody] Trainer trainer) @@ -164,60 +162,12 @@ public async Task UpdateTrainer([FromBody] Trainer trainer) return Ok(await _repository.UpdateTrainer(trainer)); } - // [Authorize(Roles = "Admin")] + [Authorize(Roles = "Admin")] [HttpDelete("{id}", Name = "DeleteTrainer")] [ProducesResponseType(typeof(Trainer), StatusCodes.Status200OK)] public async Task DeleteTrainer(string id) { return Ok(await _repository.DeleteTrainer(id)); } - - // [Authorize(Roles = "Trainer, Client")] - [Route("[action]/{id}")] - [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetTrainerScheduleByTrainerId(string id) - { - var result = await _repository.GetTrainerScheduleByTrainerId(id); - if (result == null) - { - return NotFound(); - } - return Ok(result); - } - - // [Authorize(Roles = "Trainer, Client")] - [Route("[action]/{trainerId}/{weekId}")] - [HttpGet] - [ProducesResponseType(typeof(WeeklySchedule), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetTrainerWeekSchedule(string trainerId, int weekId) - { - var result = await _repository.GetTrainerWeekSchedule(trainerId, weekId); - if (result == null) - { - return NotFound(); - } - return Ok(result); - } - - // [Authorize(Roles = "Trainer")] - [Route("[action]")] - [HttpPut] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task CancelTraining([FromBody] CancelTrainingInformation cti) - { - var result = await _repository.CancelTraining(cti); - if (result) - { - //send to client - var eventMessage = _mapper.Map(cti); - await _publishEndpoint.Publish(eventMessage); - return Ok(); - } - return BadRequest(); - } } } \ No newline at end of file diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.API/EventBusConsumers/BookTrainingConsumer.cs b/Fitness/Backend/Services/TrainerService/TrainerService.API/EventBusConsumers/BookTrainingConsumer.cs deleted file mode 100644 index 139a07f..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.API/EventBusConsumers/BookTrainingConsumer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AutoMapper; -using EventBus.Messages.Events; -using MassTransit; -using TrainerService.Common.Entities; -using TrainerService.Common.Repositories; - -namespace TrainerService.API.EventBusConsumers -{ - public class BookTrainingConsumer : IConsumer - { - private readonly ITrainerRepository _repository; - private readonly IMapper _mapper; - - public BookTrainingConsumer(ITrainerRepository repository, IMapper mapper) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - } - - public async Task Consume(ConsumeContext context) - { - var bti = _mapper.Map(context.Message); - await _repository.BookTraining(bti); - } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.API/GrpcServices/ReviewGrpcService.cs b/Fitness/Backend/Services/TrainerService/TrainerService.API/GrpcServices/ReviewGrpcService.cs index 9fd8c86..20cb36a 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.API/GrpcServices/ReviewGrpcService.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.API/GrpcServices/ReviewGrpcService.cs @@ -16,7 +16,6 @@ public async Task GetReviews(string trainerId) var getReviewsRequest = new GetReviewsRequest(); getReviewsRequest.TrainerId = trainerId; return await _reviewProtoServiceClient.GetReviewsAsync(getReviewsRequest); - } } } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.API/Program.cs b/Fitness/Backend/Services/TrainerService/TrainerService.API/Program.cs index e9ee179..4d30751 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.API/Program.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.API/Program.cs @@ -1,13 +1,9 @@ -using EventBus.Messages.Constants; -using EventBus.Messages.Events; -using MassTransit; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using ReviewService.GRPC.Protos; using System.Text; using Consul; using ConsulConfig.Settings; -using TrainerService.API.EventBusConsumers; using TrainerService.API.GrpcServices; using TrainerService.Common.Data; using TrainerService.Common.Entities; @@ -37,22 +33,6 @@ { configuration.CreateMap().ReverseMap(); configuration.CreateMap().ReverseMap(); - configuration.CreateMap().ReverseMap(); - configuration.CreateMap().ReverseMap(); -}); - -//EventBus -builder.Services.AddMassTransit(config => -{ - config.AddConsumer(); - config.UsingRabbitMq((ctx, cfg) => - { - cfg.Host(builder.Configuration["EventBusSettings:HostAddress"]); - cfg.ReceiveEndpoint(EventBusConstants.BookTrainingQueue,c => - { - c.ConfigureConsumer(ctx); - }); - }); }); builder.Services.AddCors(options => @@ -124,8 +104,8 @@ app.UseRouting(); -// app.UseAuthentication(); -// app.UseAuthorization(); +app.UseAuthentication(); +app.UseAuthorization(); app.MapControllers(); diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/ITrainerContext.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/ITrainerContext.cs index 93068ff..d89480c 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/ITrainerContext.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/ITrainerContext.cs @@ -6,6 +6,5 @@ namespace TrainerService.Common.Data public interface ITrainerContext { IMongoCollection Trainers { get; } - IMongoCollection TrainerSchedules { get; } } } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/TrainerContext.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/TrainerContext.cs index bc2b953..9aa9744 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/TrainerContext.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Data/TrainerContext.cs @@ -12,11 +12,8 @@ public TrainerContext(IConfiguration configuration) var database = client.GetDatabase("TrainerDB"); Trainers = database.GetCollection("Trainers"); - - TrainerSchedules = database.GetCollection("TrainerSchedules"); } public IMongoCollection Trainers { get; } - public IMongoCollection TrainerSchedules { get; } } } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/BookTrainingInformation.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/BookTrainingInformation.cs deleted file mode 100644 index 0204b5d..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/BookTrainingInformation.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace TrainerService.Common.Entities -{ - public class BookTrainingInformation - { - public string ClientId { get; set; } - public string TrainerId { get; set; } - public string TrainingType { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - public bool IsBooking { get; set; } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/CancelTrainingInformation.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/CancelTrainingInformation.cs deleted file mode 100644 index 6b483db..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/CancelTrainingInformation.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace TrainerService.Common.Entities -{ - public class CancelTrainingInformation - { - public string ClientId { get; set; } - public string TrainerId { get; set; } - public TimeSpan Duration { get; set; } - public int WeekId { get; set; } - public string DayName { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ReviewType.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ReviewType.cs index d47f768..6a648fb 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ReviewType.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ReviewType.cs @@ -10,7 +10,7 @@ public class ReviewType public string Id { get; set; } public string TrainerId { get; set; } public string ClientId { get; set; } - public string Comment { get; set; } - public int Rating { get; set; } + public string ClientComment { get; set; } + public int ClientRating { get; set; } } } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ScheduleItem.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ScheduleItem.cs deleted file mode 100644 index 3fa7703..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/ScheduleItem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MongoDB.Bson.Serialization.Attributes; - -namespace TrainerService.Common.Entities -{ - public class ScheduleItem - { - - [BsonIgnore] - public TimeSpan StartTime { get; set; } - [BsonIgnore] - public TimeSpan EndTime { get; set; } - public int StartHour { get; set; } - public int StartMinute { get; set; } - public int EndHour { get; set; } - public int EndMinute { get; set; } - public string TrainingType { get; set; } - public bool IsAvailable { get; set; } - public string ClientId { get; set; } - public TimeSpan TrainingDuration { get; set; } - public int TrainingStartHour { get; set; } - public int TrainingStartMinute { get; set; } - - public ScheduleItem(TimeSpan startTime, TimeSpan endTime, bool isAvailable) - { - StartTime = startTime; - EndTime = endTime; - StartHour = startTime.Hours; - StartMinute = startTime.Minutes; - EndHour = endTime.Hours; - EndMinute = endTime.Minutes; - IsAvailable = isAvailable; - TrainingType = ""; - ClientId = ""; - } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/Trainer.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/Trainer.cs index 57a3595..1957fbe 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/Trainer.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/Trainer.cs @@ -15,7 +15,7 @@ public class Trainer public List TrainingTypes { get; set; } = new List(); public List Reviews { get; set; } = new List(); [BsonIgnore] - public double AverageRating => Reviews.Any() ? Reviews.Average(r => r.Rating) : 0.0; + public double AverageRating => Reviews.Any() ? Reviews.Average(r => r.ClientRating) : 0.0; } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/TrainerSchedule.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/TrainerSchedule.cs deleted file mode 100644 index 34ccdab..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/TrainerSchedule.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson; - -namespace TrainerService.Common.Entities -{ - public class TrainerSchedule - { - [BsonId] - [BsonRepresentation(BsonType.ObjectId)] - public string Id { get; set; } - - [BsonElement("TrainerId")] - public string TrainerId { get; set; } - public List WeeklySchedules { get; set; } = new List(); - public TrainerSchedule(string id) - { - var startWeek = 1; - var endWeek = 3; - TrainerId = id; - for (int i = startWeek; i <= endWeek; i++) - { - WeeklySchedules.Add(new WeeklySchedule(i)); - } - } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/WeeklySchedule.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/WeeklySchedule.cs deleted file mode 100644 index 01a1185..0000000 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Entities/WeeklySchedule.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace TrainerService.Common.Entities -{ - public class WeeklySchedule - { - public int WeekId { get; set; } - public Dictionary> DailySchedules { get; set; } = new Dictionary>(); - - private static readonly Dictionary DayName = new Dictionary - { - { 1, "Monday" }, - { 2, "Tuesday" }, - { 3, "Wednesday" }, - { 4, "Thursday" }, - { 5, "Friday" }, - { 6, "Saturday" }, - { 7, "Sunday" } - }; - public WeeklySchedule(int weekId) - { - WeekId = weekId; - - for (int i = 1; i <= 7; ++i) - { - DailySchedules[DayName[i]] = InitializeDay(); - } - } - - private static List InitializeDay() - { - var timeslots = new List(); - var startTime = new TimeSpan(8, 0, 0); // 8:00 AM - var endTime = new TimeSpan(20, 0, 0); // 8:00 PM - - while (startTime < endTime) - { - var nextTime = startTime.Add(TimeSpan.FromMinutes(15)); - timeslots.Add(new ScheduleItem(startTime, nextTime, true)); - startTime = nextTime; - } - return timeslots; - } - } -} diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/ITrainerRepository.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/ITrainerRepository.cs index fb69b01..33b51fa 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/ITrainerRepository.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/ITrainerRepository.cs @@ -14,10 +14,5 @@ public interface ITrainerRepository Task CreateTrainer(Trainer trainer); Task UpdateTrainer(Trainer trainer); Task DeleteTrainer(string id); - Task GetTrainerScheduleByTrainerId(string id); - Task GetTrainerWeekSchedule(string trainerId, int weekId); - Task UpdateTrainerSchedule(TrainerSchedule trainerSchedule); - Task BookTraining(BookTrainingInformation bti); - Task CancelTraining(CancelTrainingInformation cti); } } diff --git a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/TrainerRepository.cs b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/TrainerRepository.cs index e48b2e4..0614733 100644 --- a/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/TrainerRepository.cs +++ b/Fitness/Backend/Services/TrainerService/TrainerService.Common/Repositories/TrainerRepository.cs @@ -56,13 +56,10 @@ public async Task GetPrice(string trainerId, string trainingType) return training.Price; } - - + public async Task CreateTrainer(Trainer trainer) { await _context.Trainers.InsertOneAsync(trainer); - var trainerSchedule = new TrainerSchedule(trainer.Id); - await _context.TrainerSchedules.InsertOneAsync(trainerSchedule); } public async Task UpdateTrainer(Trainer trainer) @@ -74,91 +71,7 @@ public async Task UpdateTrainer(Trainer trainer) public async Task DeleteTrainer(string id) { var deleteResult = await _context.Trainers.DeleteOneAsync(p => p.Id == id); - var deleteScheduleResult = await _context.TrainerSchedules.DeleteOneAsync(ts => ts.Id == id); - return deleteResult.IsAcknowledged && deleteResult.DeletedCount > 0 - && deleteScheduleResult.IsAcknowledged && deleteResult.DeletedCount>0; - } - - public async Task GetTrainerScheduleByTrainerId(string id) - { - return await _context.TrainerSchedules.Find(s => s.TrainerId == id).FirstOrDefaultAsync(); - } - public async Task GetTrainerWeekSchedule(string trainerId, int weekId) - { - var trainerSchedule = await GetTrainerScheduleByTrainerId(trainerId); - return trainerSchedule?.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == weekId); - } - public async Task UpdateTrainerSchedule(TrainerSchedule trainerSchedule) - { - var result = await _context.TrainerSchedules.ReplaceOneAsync(cs => cs.TrainerId == trainerSchedule.TrainerId, trainerSchedule, new ReplaceOptions { IsUpsert = true }); - return result.IsAcknowledged && result.ModifiedCount > 0; - } - public async Task BookTraining(BookTrainingInformation bti) - { - var trainerSchedule = await GetTrainerScheduleByTrainerId(bti.TrainerId); - var weeklySchedule = trainerSchedule.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == bti.WeekId); - //provereno na frontu da moze da se zakaze - weeklySchedule.DailySchedules.TryGetValue(bti.DayName, out var dailySchedule); - - int numberOfCells = (int)bti.Duration.TotalMinutes / 15; - var startSlotIndex = dailySchedule.FindIndex(slot => slot.StartHour == bti.StartHour && slot.StartMinute == bti.StartMinute); - for (int i = startSlotIndex; i < startSlotIndex + numberOfCells; i++) - { - if (bti.IsBooking) - { - dailySchedule[i].IsAvailable = false; - dailySchedule[i].ClientId = bti.ClientId; - dailySchedule[i].TrainingType = bti.TrainingType; - dailySchedule[i].TrainingDuration = bti.Duration; - dailySchedule[i].TrainingStartHour = bti.StartHour; - dailySchedule[i].TrainingStartMinute = bti.StartMinute; - } - else - { - dailySchedule[i].IsAvailable = true; - dailySchedule[i].ClientId = ""; - dailySchedule[i].TrainingType = ""; - dailySchedule[i].TrainingStartHour = -1; - } - } - await UpdateTrainerSchedule(trainerSchedule); - } - - public async Task CancelTraining(CancelTrainingInformation cti) - { - var trainerSchedule = await GetTrainerScheduleByTrainerId(cti.TrainerId); - if(trainerSchedule == null) - { - return false; - } - - var weeklySchedule = trainerSchedule.WeeklySchedules.FirstOrDefault(ws => ws.WeekId == cti.WeekId); - if(weeklySchedule == null) - { - return false; - } - - if (!weeklySchedule.DailySchedules.TryGetValue(cti.DayName, out var dailySchedule)) - { - return false; - } - - int numberOfCells = (int)cti.Duration.TotalMinutes / 15; - - var startSlotIndex = dailySchedule.FindIndex(slot => slot.StartHour == cti.StartHour && slot.StartMinute == cti.StartMinute); - if (startSlotIndex == -1 || startSlotIndex + numberOfCells > dailySchedule.Count) - return false; - for (int i = startSlotIndex; i < startSlotIndex + numberOfCells; i++) - { - dailySchedule[i].IsAvailable = true; - dailySchedule[i].ClientId = ""; - dailySchedule[i].TrainingType = ""; - //dailySchedule[i].TrainingStartHour = -1; - } - - await UpdateTrainerSchedule(trainerSchedule); - - return true; + return deleteResult.IsAcknowledged && deleteResult.DeletedCount > 0; } } } diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/TrainingController.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/TrainingController.cs new file mode 100644 index 0000000..4c29f06 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/TrainingController.cs @@ -0,0 +1,204 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using videoTrainingService.API.Entities; +using videoTrainingService.API.Repositories; + +namespace videoTrainingService.API.Controllers +{ + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + + public class TrainingController : ControllerBase + { + private readonly ITrainingRepository _repository; + + public TrainingController(ITrainingRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("exercises/{trainerId}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetExercises(string trainerId) + { + var exercises = await _repository.GetExercises(trainerId); + return Ok(exercises); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("exercise/{id}", Name = "GetExercise")] + [ProducesResponseType(typeof(Exercise), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetExercise(string id) + { + var exercise = await _repository.GetExercise(id); + if (exercise == null) + { + return NotFound(); + } + return Ok(exercise); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPost("exercise")] + [ProducesResponseType(typeof(Exercise), StatusCodes.Status201Created)] + public async Task CreateExercise([FromBody] Exercise exercise) + { + await _repository.CreateExercise(exercise); + return CreatedAtRoute("GetExercise", new { id = exercise.Id} , exercise); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPut("exercise")] + [ProducesResponseType(typeof(Exercise), StatusCodes.Status200OK)] + public async Task UpdateExercise([FromBody] Exercise exercise) + { + var result = await _repository.UpdateExercise(exercise); + return Ok(result); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpDelete("exercise/{id}")] + [ProducesResponseType(typeof(Exercise), StatusCodes.Status200OK)] + public async Task DeleteExercise(string id) + { + var result = await _repository.DeleteExercise(id); + return Ok(result); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("training/trainingClient", Name = "GetTrainingsForClient")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetTrainingsForClient() + { + var trainings = await _repository.GetTrainingsForClient(); + return Ok(trainings); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("training/trainingTrainer/{trainerId}", Name = "GetTrainingsForTrainer")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetTrainingsForTrainer(string trainerId) + { + var trainings = await _repository.GetTrainingsForTrainer(trainerId); + return Ok(trainings); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("training/{id}", Name = "GetTraining")] + [ProducesResponseType(typeof(Training), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetTraining(string id) + { + var training = await _repository.GetTraining(id); + if (training == null) + { + return NotFound(); + } + return Ok(training); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPost("training")] + [ProducesResponseType(typeof(Training), StatusCodes.Status201Created)] + public async Task CreateTraining([FromBody] Training training) + { + await _repository.CreateTraining(training); + return CreatedAtRoute("GetTraining", new { id = training.TrainingId} , training); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPut("training")] + [ProducesResponseType(typeof(Training), StatusCodes.Status200OK)] + public async Task UpdateTraining([FromBody] Training training) + { + var result = await _repository.UpdateTraining(training); + return Ok(result); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpDelete("training/{id}")] + [ProducesResponseType(typeof(Training), StatusCodes.Status200OK)] + public async Task DeleteTraining(string id) + { + var result = await _repository.DeleteTraining(id); + return Ok(result); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("trainingExercises/{trainingId}", Name = "GetTrainingExercises")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> GetTrainingExercises(string trainingId) + { + var trainingExercises = await _repository.GetTrainingExercises(trainingId); + return Ok(trainingExercises); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("trainingExercise/{id}", Name = "GetTrainingExercise")] + [ProducesResponseType(typeof(TrainingExercise), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetTrainingExercise(string id) + { + var trainingExercise = await _repository.GetTrainingExercise(id); + if (trainingExercise == null) + { + return NotFound(); + } + return Ok(trainingExercise); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPost("trainingExercise")] + [ProducesResponseType(typeof(TrainingExercise), StatusCodes.Status201Created)] + public async Task CreateTrainingExercise([FromBody] TrainingExercise trainingExercise) + { + await _repository.CreateTrainingExercise(trainingExercise); + return CreatedAtRoute("GetTrainingExercise", new { id = trainingExercise.Id} , trainingExercise); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpPut("trainingExercise")] + [ProducesResponseType(typeof(TrainingExercise), StatusCodes.Status200OK)] + public async Task UpdateTrainingExercise([FromBody] TrainingExercise trainingExercises) + { + var result = await _repository.UpdateTrainingExercise(trainingExercises); + return Ok(result); + } + + [Authorize(Roles = "Admin, Trainer")] + [HttpDelete("trainingExercise/{trainingId}")] + [ProducesResponseType(typeof(TrainingExercise), StatusCodes.Status200OK)] + public async Task DeleteTrainingExercises(string trainingId) + { + var result = await _repository.DeleteTrainingExercises(trainingId); + return Ok(result); + } + + [Authorize(Roles = "Client")] + [HttpPost("training/{trainingId}/addClient/{clientId}")] + public async Task AddClientToTraining(string trainingId, string clientId) + { + var success = await _repository.AddClientToTraining(trainingId, clientId); + + if (!success) + return BadRequest("Client could not be added to training (maybe already exists or training not found)."); + + return Ok($"Client {clientId} added to training {trainingId}."); + } + + [Authorize(Roles = "Admin, Trainer, Client")] + [HttpGet("training/byClient/{clientId}")] + public async Task>> GetTrainingsByClient(string clientId) + { + var trainings = await _repository.GetTrainingsByClient(clientId); + + if (trainings == null || trainings.Count == 0) + return NotFound($"No trainings found for client {clientId}."); + + return Ok(trainings); + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/UploadController.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/UploadController.cs new file mode 100644 index 0000000..81a696d --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Controllers/UploadController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace videoTrainingService.API.Controllers + +{ + + [Authorize] + [ApiController] + [Route("api/v1/[controller]")] + public class UploadController : ControllerBase + { + private readonly string _uploadsPath; + + public UploadController(IWebHostEnvironment env) + { + _uploadsPath = Path.Combine(env.ContentRootPath, "uploads"); + if (!Directory.Exists(_uploadsPath)) + { + Directory.CreateDirectory(_uploadsPath); + } + } + + [Authorize(Roles="Admin, Trainer")] + [HttpPost("video")] + [ApiExplorerSettings(IgnoreApi = true)] + public async Task UploadVideo([FromForm] IFormFile file) + { + if (file == null || file.Length == 0) + return BadRequest("No file uploaded"); + + var filePath = Path.Combine(_uploadsPath, file.FileName); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { FileName = file.FileName }); + } + + [Authorize(Roles="Admin, Trainer")] + [HttpDelete("video/delete/{fileName}")] + public IActionResult DeleteVideo(string fileName) + { + try + { + if (string.IsNullOrEmpty(fileName)) + return BadRequest("Invalid file name."); + + var filePath = Path.Combine(_uploadsPath, fileName); + + if (!System.IO.File.Exists(filePath)) + return NotFound("File not found."); + + System.IO.File.Delete(filePath); + + return Ok(new { message = $"File '{fileName}' successfully deleted." }); + } + catch (Exception ex) + { + return StatusCode(500, $"Error deleting file: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/ITrainingContext.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/ITrainingContext.cs new file mode 100644 index 0000000..fa5dddf --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/ITrainingContext.cs @@ -0,0 +1,12 @@ +using MongoDB.Driver; +using videoTrainingService.API.Entities; + +namespace videoTrainingService.API.Data +{ + public interface ITrainingContext + { + IMongoCollection Exercises { get; } + IMongoCollection Trainings { get; } + IMongoCollection TrainingExercises { get; } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/TrainingContext.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/TrainingContext.cs new file mode 100644 index 0000000..410b1a1 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Data/TrainingContext.cs @@ -0,0 +1,22 @@ +using MongoDB.Driver; +using videoTrainingService.API.Entities; + +namespace videoTrainingService.API.Data +{ + public class TrainingContext : ITrainingContext + { + public TrainingContext(IConfiguration configuration) + { + var client = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString")); + var database = client.GetDatabase("TrainingDB"); + + Exercises = database.GetCollection("Exercises"); + Trainings = database.GetCollection("Trainings"); + TrainingExercises = database.GetCollection("TrainingExercises"); + } + + public IMongoCollection Exercises { get; } + public IMongoCollection Trainings { get; } + public IMongoCollection TrainingExercises { get; } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Dockerfile b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Dockerfile new file mode 100644 index 0000000..eea1da6 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Dockerfile @@ -0,0 +1,27 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base + +WORKDIR /app +RUN mkdir /app/uploads +RUN chmod 777 /app/uploads +RUN chown -R $APP_UID /app/uploads + +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.csproj", "Services/videoTrainingService/videoTrainingService.API/"] +RUN dotnet restore "Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.csproj" +COPY . . +WORKDIR "/src/Services/videoTrainingService/videoTrainingService.API" +RUN dotnet build "videoTrainingService.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "videoTrainingService.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "videoTrainingService.API.dll"] diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Exercise.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Exercise.cs new file mode 100644 index 0000000..3f4e184 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Exercise.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace videoTrainingService.API.Entities +{ + public class Exercise + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string TrainerId { get; set; } + public string Name { get; set; } + public string Path { get; set; } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Training.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Training.cs new file mode 100644 index 0000000..a3e1dc0 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/Training.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace videoTrainingService.API.Entities +{ + public class Training + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string TrainingId { get; set; } + public string TrainerId { get; set; } + public string Type { get; set; } + public string Description { get; set; } + public IEnumerable ClientIds { get; set;} + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/TrainingExercise.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/TrainingExercise.cs new file mode 100644 index 0000000..ce675d4 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Entities/TrainingExercise.cs @@ -0,0 +1,18 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace videoTrainingService.API.Entities +{ + public class TrainingExercise + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string TrainerId { get; set; } + public string TrainingId { get; set; } + public string ExerciseId { get; set; } + public int ExerciseReps { get; set; } + public int Set { get; set; } + public int SetReps { get; set; } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Program.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Program.cs new file mode 100644 index 0000000..39379b0 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Program.cs @@ -0,0 +1,117 @@ +using Consul; +using ConsulConfig.Settings; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using videoTrainingService.API.Data; +using videoTrainingService.API.Repositories; +using Microsoft.Extensions.FileProviders; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); +var consulConfig = builder.Configuration.GetSection("ConsulConfig").Get()!; +builder.Services.AddSingleton(consulConfig); +builder.Services.AddSingleton(provider => new ConsulClient(config => +{ + config.Address = new Uri(consulConfig.Address); +})); + +// Add services to the container. + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", + policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +builder.Services.AddControllers(); + +var jwtSettings = builder.Configuration.GetSection("JwtSettings"); +var secretKey = jwtSettings.GetValue("secretKey"); + +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)) + }; + }); + +var app = builder.Build(); + +app.Lifetime.ApplicationStarted.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + + var registration = new AgentServiceRegistration + { + ID = consulConfig.ServiceId, + Name = consulConfig.ServiceName, + Address = consulConfig.ServiceAddress, + Port = consulConfig.ServicePort + }; + + consulClient.Agent.ServiceRegister(registration).Wait(); +}); + +app.Lifetime.ApplicationStopping.Register(() => +{ + var consulClient = app.Services.GetRequiredService(); + consulClient.Agent.ServiceDeregister(consulConfig.ServiceId).Wait(); +}); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads"); +if (!Directory.Exists(uploadsPath)) +{ + Directory.CreateDirectory(uploadsPath); +} + +app.UseStaticFiles(new StaticFileOptions +{ + FileProvider = new PhysicalFileProvider(uploadsPath), + RequestPath = "/uploads" +}); + +app.UseCors("AllowAll"); + +app.UseRouting(); +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.Run(); + diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Properties/launchSettings.json b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Properties/launchSettings.json new file mode 100644 index 0000000..4f55feb --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Properties/launchSettings.json @@ -0,0 +1,50 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5275", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "VideoTrainingService.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5004" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true + } + }, + + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21255", + "sslPort": 44336 + } + } +} diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/ITrainingRepository.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/ITrainingRepository.cs new file mode 100644 index 0000000..bef62ce --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/ITrainingRepository.cs @@ -0,0 +1,26 @@ +using videoTrainingService.API.Entities; + +namespace videoTrainingService.API.Repositories +{ + public interface ITrainingRepository + { + Task> GetExercises(string trainerId); + Task GetExercise(string id); + Task CreateExercise(Exercise exercise); + Task UpdateExercise(Exercise exercise); + Task DeleteExercise(string id); + Task> GetTrainingsForClient(); + Task> GetTrainingsForTrainer(string trainerId); + Task GetTraining(string id); + Task CreateTraining(Training training); + Task UpdateTraining(Training training); + Task DeleteTraining(string id); + Task> GetTrainingExercises(string trainingId); + Task GetTrainingExercise(string id); + Task CreateTrainingExercise(TrainingExercise trainingExercise); + Task UpdateTrainingExercise(TrainingExercise trainingExercise); + Task DeleteTrainingExercises(string trainingId); + Task AddClientToTraining(string trainingId, string clientId); + Task> GetTrainingsByClient(string clientId); + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/TrainingRepository.cs b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/TrainingRepository.cs new file mode 100644 index 0000000..a142ca9 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/Repositories/TrainingRepository.cs @@ -0,0 +1,124 @@ +using MongoDB.Driver; +using videoTrainingService.API.Data; +using videoTrainingService.API.Entities; + +namespace videoTrainingService.API.Repositories +{ + public class TrainingRepository : ITrainingRepository + { + private readonly ITrainingContext _context; + + public TrainingRepository(ITrainingContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public async Task> GetExercises(string trainerId) + { + return await _context.Exercises.Find(p => p.TrainerId == trainerId).ToListAsync(); + } + + public async Task GetExercise(string id) + { + return await _context.Exercises.Find(p => p.Id == id).FirstOrDefaultAsync(); + } + + public async Task CreateExercise(Exercise exercise) + { + await _context.Exercises.InsertOneAsync(exercise); + } + + public async Task UpdateExercise(Exercise exercise) + { + var res = await _context.Exercises.ReplaceOneAsync(p => p.Id == exercise.Id, exercise); + return res.IsAcknowledged && res.ModifiedCount > 0; + } + + public async Task DeleteExercise(string id) + { + var res = await _context.Exercises.DeleteOneAsync(p => p.Id == id); + return res.IsAcknowledged && res.DeletedCount > 0; + } + + public async Task> GetTrainingsForClient() + { + return await _context.Trainings.Find(_ => true).ToListAsync(); + } + + public async Task> GetTrainingsForTrainer(string trainerId) + { + return await _context.Trainings.Find(p => p.TrainerId == trainerId).ToListAsync(); + } + + public async Task GetTraining(string id) + { + return await _context.Trainings.Find(p => p.TrainingId == id).FirstOrDefaultAsync(); + } + + public async Task CreateTraining(Training training) + { + await _context.Trainings.InsertOneAsync(training); + } + + public async Task UpdateTraining(Training training) + { + var res = await _context.Trainings.ReplaceOneAsync(p => p.TrainingId == training.TrainingId, training); + return res.IsAcknowledged && res.ModifiedCount > 0; + } + + public async Task DeleteTraining(string id) + { + var res = await _context.Trainings.DeleteOneAsync(p => p.TrainingId == id); + return res.IsAcknowledged && res.DeletedCount > 0; + } + + public async Task> GetTrainingExercises(string trainingId) + { + return await _context.TrainingExercises.Find(p => p.TrainingId == trainingId).ToListAsync(); + } + + public async Task GetTrainingExercise(string id) + { + return await _context.TrainingExercises.Find(p => p.Id == id).FirstOrDefaultAsync(); + } + + public async Task CreateTrainingExercise(TrainingExercise trainingExercise) + { + await _context.TrainingExercises.InsertOneAsync(trainingExercise); + } + + public async Task UpdateTrainingExercise(TrainingExercise trainingExercise) + { + var res = await _context.TrainingExercises.ReplaceOneAsync(p => p.TrainingId == trainingExercise.TrainingId && + p.ExerciseId == trainingExercise.ExerciseId, trainingExercise); + return res.IsAcknowledged && res.ModifiedCount > 0; + } + + public async Task DeleteTrainingExercises(string trainingId) + { + var res = await _context.TrainingExercises.DeleteManyAsync(p => p.TrainingId == trainingId); + return res.IsAcknowledged && res.DeletedCount > 0; + } + + public async Task AddClientToTraining(string trainingId, string clientId) + { + var filter = Builders.Filter.Eq(t => t.TrainingId, trainingId); + var update = Builders.Update.AddToSet(t => t.ClientIds, clientId); + + var result = await _context.Trainings.UpdateOneAsync(filter, update); + + return result.IsAcknowledged && result.ModifiedCount > 0; + } + + public async Task> GetTrainingsByClient(string clientId) + { + var filter = Builders.Filter.AnyEq(t => t.ClientIds, clientId); + + var trainings = await _context.Trainings + .Find(filter) + .ToListAsync(); + + return trainings; + } + } +} \ No newline at end of file diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.Development.json b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.Development.json new file mode 100644 index 0000000..55033f4 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.Development.json @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "JwtSettings": { + "validIssuer": "Fitness Identity", + "validAudience": "Fitness", + "secretKey": "MyVeryVerySecretMessageForSecretKey", + "expires": 15 + }, + "DatabaseSettings": { + "ConnectionString": "mongodb://videotrainingdb:27017" + }, + "ConsulConfig": { + "Address": "http://consul:8500", + "ServiceName": "VideoTrainingService.API", + "ServiceId": "VideoTrainingService.API-1", + "ServiceAddress": "videotrainingservice.api", + "ServicePort": 8080 + } +} + + diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.json b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.csproj b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.csproj new file mode 100644 index 0000000..1802963 --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + + + + ..\..\..\..\..\..\..\.nuget\packages\mongodb.bson\2.28.0\lib\netstandard2.1\MongoDB.Bson.dll + + + ..\..\..\..\..\..\..\.nuget\packages\mongodb.driver\2.28.0\lib\netstandard2.1\MongoDB.Driver.dll + + + + + + .dockerignore + + + + + + + + diff --git a/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.http b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.http new file mode 100644 index 0000000..332b17d --- /dev/null +++ b/Fitness/Backend/Services/videoTrainingService/videoTrainingService.API/videoTrainingService.API.http @@ -0,0 +1,6 @@ +@videoTrainingService.API_HostAddress = http://localhost:5275 + +GET {{videoTrainingService.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Fitness/Backend/docker-compose.development.yml b/Fitness/Backend/docker-compose.development.yml index 808caf1..4ac7b3d 100644 --- a/Fitness/Backend/docker-compose.development.yml +++ b/Fitness/Backend/docker-compose.development.yml @@ -1,29 +1,47 @@ +version: "3.9" + services: - trainerdb: - container_name: trainerdb + # === Databases === + analyticsdb: + container_name: analyticsdb restart: always ports: - - "27017:27017" + - "27027:27017" volumes: - - trainermongo_data:/data/db + - analyticsmongo_data:/data/db + chatdb: + container_name: chatdb + restart: always + ports: + - "27022:27017" + volumes: + - chatmongo_data:/data/db clientdb: container_name: clientdb restart: always ports: - "27019:27017" - volumes: + volumes: - clientmongo_data:/data/db - reviewdb: - container_name: reviewdb + notificationdb: + container_name: notificationdb restart: always ports: - - "27018:27017" + - "27021:27017" volumes: - - reviewmongo_data:/data/db - + - notificationmongo_data:/data/db + + nutritiondb: + container_name: nutritiondb + restart: always + ports: + - "27037:27017" + volumes: + - nutritionmongo_data:/data/db + paymentdb: container_name: paymentdb restart: always @@ -31,78 +49,96 @@ services: - "27020:27017" volumes: - paymentmongo_data:/data/db - - notificationdb: - container_name: notificationdb + + reservationdb: + container_name: reservationdb restart: always ports: - - "27021:27017" + - "27023:27017" volumes: - - notificationmongo_data:/data/db - - chatdb: - container_name: chatdb + - reservationmongo_data:/data/db + + reviewdb: + container_name: reviewdb + restart: always + ports: + - "27018:27017" + volumes: + - reviewmongo_data:/data/db + + trainerdb: + container_name: trainerdb restart: always ports: - - 27022:27017 + - "27017:27017" volumes: - - chatmongo_data:/data/db - + - trainermongo_data:/data/db + + videotrainingdb: + container_name: videotrainingdb + restart: always + ports: + - "27030:27017" + volumes: + - videotrainingmongo_data:/data/db + + # === Infrastructure === + consul: + container_name: consul + ports: + - "8500:8500" + command: "agent -dev -client=0.0.0.0" + volumes: + - consul_data:/consul/data + rabbitmq: container_name: rabbitmq restart: always ports: - "5672:5672" - "15672:15672" - + mssql: container_name: mssql environment: - SA_PASSWORD=MATF12345678rs2 - ACCEPT_EULA=Y restart: always + user: root ports: - "1433:1433" volumes: - mssql_data:/var/opt/mssql/ - mssql_volume:/var/opt/sqlserver/ - - consul: - container_name: consul - ports: - - "8500:8500" - command: "agent -dev -client=0.0.0.0" - volumes: - - consul_data:/consul/data - - + + # === Gateway === gatewayservice.api: container_name: gatewayservice.api environment: - ASPNETCORE_ENVIRONMENT=Development ports: - - "8005:8080" - + - "8005:8080" + + # === Trainer Service === trainerservice.api: container_name: trainerservice.api environment: - ASPNETCORE_ENVIRONMENT=Development - "DatabaseSettings:ConnectionString=mongodb://trainerdb:27017" - "GrpcSettings:ReviewUrl=http://reviewservice.grpc:8080" - - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" + - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" ports: - "8000:8080" - + trainerservice.grpc: container_name: trainerservice.grpc environment: - ASPNETCORE_ENVIRONMENT=Development - "DatabaseSettings:ConnectionString=mongodb://trainerdb:27017" - depends_on: - - trainerdb ports: - "8102:8080" + # === Client Service === clientservice.api: container_name: clientservice.api environment: @@ -111,17 +147,16 @@ services: - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" ports: - "8100:8080" - + clientservice.grpc: container_name: clientservice.grpc environment: - ASPNETCORE_ENVIRONMENT=Development - "DatabaseSettings:ConnectionString=mongodb://clientdb:27017" - depends_on: - - reviewdb ports: - "8101:8080" + # === Identity Server === identityserver: container_name: identityserver environment: @@ -130,11 +165,13 @@ services: ports: - "4000:8080" + # === Review Service === reviewservice.api: container_name: reviewservice.api environment: - ASPNETCORE_ENVIRONMENT=Development - "DatabaseSettings:ConnectionString=mongodb://reviewdb:27017" + - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" ports: - "8001:8080" @@ -146,16 +183,18 @@ services: ports: - "8002:8080" + # === Payment Service === paymentservice.api: container_name: paymentservice.api environment: - ASPNETCORE_ENVIRONMENT=Development - "DatabaseSettings:ConnectionString=mongodb://paymentdb:27017" - - PAYPAL_CLIENT_ID="PayPalSettings:ClientId" - - PAYPAL_CLIENT_SECRET="PayPalSettings:ClientSecret" + env_file: + - ./Services/PaymentService/PaymentService.API/.env ports: - "8003:8080" - + + # === Notification Service === notificationservice.api: container_name: notificationservice.api environment: @@ -171,13 +210,20 @@ services: - "GrpcSettings:TrainerUrl=http://host.docker.internal:8102" env_file: - ./Services/NotificationService/NotificationService.API/.env - depends_on: - - notificationdb - - rabbitmq ports: - "8004:8080" - + # === Analytics Service === + analyticsservice.api: + container_name: analyticsservice.api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - "DatabaseSettings:ConnectionString=mongodb://analyticsdb:27017" + - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" + ports: + - "8018:8080" + + # === Chat Service === chatservice.api: container_name: chatService.api environment: @@ -185,4 +231,58 @@ services: - "MongoDB:ConnectionString=mongodb://chatdb:27017" - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" ports: - - "8082:8080" \ No newline at end of file + - "8082:8080" + + # === Reservation Service === + reservationservice.api: + container_name: reservationservice.api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - "DatabaseSettings:ConnectionString=mongodb://reservationdb:27017" + - "EventBusSettings:HostAddress=amqp://guest:guest@rabbitmq:5672" + ports: + - "8103:8080" + + # === Video Training Service === + videotrainingservice.api: + container_name: videotrainingservice.api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - "DatabaseSetting:ConnectionString=mongodb://videotrainingdb:27017" + ports: + - "8007:8080" + + # === Nutrition Service === + nutritionservice.api: + container_name: nutritionservice.api + build: + context: . + dockerfile: Services/NutritionService/NutritionService.API/Dockerfile + #volumes: + # - ./Services/NutritionService/NutritionService.API:/app + environment: + - DOTNET_USE_POLLING_FILE_WATCHER=1 + - DOTNET_RUNNING_IN_CONTAINER=true + - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + - "DatabaseSettings:ConnectionString=mongodb://nutritiondb:27017" + - "DatabaseSettings:DatabaseName=NutritionDb" + ports: + - "8157:8080" + +volumes: + analyticsmongo_data: + chatmongo_data: + clientmongo_data: + consul_data: + mssql_data: + mssql_volume: + notificationmongo_data: + nutritionmongo_data: + paymentmongo_data: + reservationmongo_data: + reviewmongo_data: + trainermongo_data: + videotrainingmongo_data: + diff --git a/Fitness/Backend/docker-compose.yml b/Fitness/Backend/docker-compose.yml index 3960d0c..5ae3281 100644 --- a/Fitness/Backend/docker-compose.yml +++ b/Fitness/Backend/docker-compose.yml @@ -1,38 +1,57 @@ +version: "3.9" + services: - trainerdb: + # === Databases === + analyticsdb: + image: mongo + + chatdb: image: mongo - mssql: - image: mcr.microsoft.com/mssql/server:2022-latest clientdb: image: mongo - reviewdb: + notificationdb: + image: mongo + + nutritiondb: image: mongo paymentdb: image: mongo - - notificationdb: + + reservationdb: image: mongo - - chatdb: + + reviewdb: image: mongo - - rabbitmq: - image: rabbitmq:3-management-alpine - + + trainerdb: + image: mongo + + videotrainingdb: + image: mongo + + # === Infrastructure === consul: image: consul:1.15.4 - + + mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + + rabbitmq: + image: rabbitmq:3-management-alpine + + # === Gateway === gatewayservice.api: - image: ${DOCKER_REGIRSTRY-}gatewayserviceapi + image: ${DOCKER_REGISTRY-}gatewayserviceapi build: context: . dockerfile: Services/GatewayService/GatewayService.API/Dockerfile depends_on: - consul - + + # === Trainer Service === trainerservice.api: image: ${DOCKER_REGISTRY-}trainerserviceapi build: @@ -40,16 +59,18 @@ services: dockerfile: Services/TrainerService/TrainerService.API/Dockerfile depends_on: - trainerdb - - rabbitmq + - reviewservice.grpc - consul - - + trainerservice.grpc: image: ${DOCKER_REGISTRY-}trainerservicegrpc build: context: . dockerfile: Services/TrainerService/TrainerService.GRPC/Dockerfile - + depends_on: + - trainerdb + + # === Client Service === clientservice.api: image: ${DOCKER_REGISTRY-}clientserviceapi build: @@ -57,16 +78,18 @@ services: dockerfile: Services/ClientService/ClientService.API/Dockerfile depends_on: - clientdb - - rabbitmq - consul - - + clientservice.grpc: image: ${DOCKER_REGISTRY-}clientservicegrpc build: context: . dockerfile: Services/ClientService/ClientService.GRPC/Dockerfile + depends_on: + - clientdb + - consul + # === Identity Service === identityserver: image: ${DOCKER_REGISTRY-}identityserver build: @@ -76,6 +99,7 @@ services: - consul - mssql + # === Review Service === reviewservice.api: image: ${DOCKER_REGISTRY-}reviewserviceapi build: @@ -83,6 +107,7 @@ services: dockerfile: Services/ReviewService/ReviewService.API/Dockerfile depends_on: - reviewdb + - rabbitmq - consul reviewservice.grpc: @@ -94,38 +119,92 @@ services: - reviewdb - consul + # === Payment Service === paymentservice.api: image: ${DOCKER_REGISTRY-}paymentserviceapi build: context: . dockerfile: Services/PaymentService/PaymentService.API/Dockerfile + depends_on: + - consul + + # === Reservation Service === + reservationservice.api: + image: ${DOCKER_REGISTRY-}reservationserviceapi + build: + context: . + dockerfile: Services/ReservationService/ReservationService.API/Dockerfile depends_on: - paymentdb - consul + - rabbitmq + + # === Analytics Service === + analyticsservice.api: + image: ${DOCKER_REGISTRY-}analyticsserviceapi + build: + context: . + dockerfile: Services/AnalyticsService/AnalyticsService.API/Dockerfile + depends_on: + - analyticsdb + - consul + - rabbitmq + # === Notification Service === notificationservice.api: image: ${DOCKER_REGISTRY-}notificationserviceapi build: context: . dockerfile: Services/NotificationService/NotificationService.API/Dockerfile - + depends_on: + - consul + - notificationdb + - rabbitmq + + # === Chat Service === chatservice.api: image: ${DOCKER_REGISTRY-}chatserviceapi build: context: . - dockerfile: Services/ChatService.API/Dockerfile + dockerfile: Services/ChatService/ChatService.API/Dockerfile depends_on: - chatdb - rabbitmq - consul + # === Nutrition Service === + nutritionservice.api: + image: ${DOCKER_REGISTRY-}nutritionserviceapi + build: + context: . + dockerfile: Services/NutritionService/NutritionService.API/Dockerfile + depends_on: + - nutritiondb + - consul + + # === Video Training Service === + videotrainingservice.api: + image: ${DOCKER_REGISTRY-}videotrainingserviceapi + build: + context: . + dockerfile: Services/videoTrainingService/videoTrainingService.API/Dockerfile + depends_on: + - videotrainingdb + - consul + volumes: + - ./uploads:/app/uploads + volumes: - trainermongo_data: - reviewmongo_data: - clientmongo_data: - paymentmongo_data: - notificationmongo_data: + analyticsmongo_data: chatmongo_data: + clientmongo_data: + consul_data: mssql_data: mssql_volume: - consul_data: \ No newline at end of file + notificationmongo_data: + nutritionmongo_data: + paymentmongo_data: + reservationmongo_data: + reviewmongo_data: + trainermongo_data: + videotrainingmongo_data: \ No newline at end of file diff --git a/Fitness/Backend/launchSettings.json b/Fitness/Backend/launchSettings.json index 59bfdea..608c8a9 100644 --- a/Fitness/Backend/launchSettings.json +++ b/Fitness/Backend/launchSettings.json @@ -8,7 +8,8 @@ "clientservice.api": "StartDebugging", "reviewservice.api": "StartDebugging", "reviewservice.grpc": "StartDebugging", - "paymentservice.api": "StartDebugging" + "paymentservice.api": "StartDebugging", + "videotrainingservice.api": "StartDebugging" } } } diff --git a/Fitness/Frontend/package.json b/Fitness/Frontend/package.json index 5670a52..93fe07d 100644 --- a/Fitness/Frontend/package.json +++ b/Fitness/Frontend/package.json @@ -25,10 +25,18 @@ "@coreui/icons-vue": "2.0.0", "@coreui/utils": "^1.3.1", "@coreui/vue": "^4.5.0", - "@coreui/vue-chartjs": "2.0.1", + "@coreui/vue-chartjs": "^3.0.0", + "@fullcalendar/core": "^6.1.19", + "@fullcalendar/daygrid": "^6.1.19", + "@fullcalendar/interaction": "^6.1.19", + "@fullcalendar/timegrid": "^6.1.19", + "@fullcalendar/vue3": "^6.1.19", "axios": "^1.4.0", + "chart.js": "^4.5.0", "core-js": "^3.26.1", + "jwt-decode": "^4.0.0", "vue": "^3.2.45", + "vue-chartjs": "^5.3.2", "vue-loading-overlay": "^6.0.3", "vue-router": "^4.1.6", "vuex": "^4.1.0" diff --git a/Fitness/Frontend/src/_nav.js b/Fitness/Frontend/src/_nav.js index 045bfca..8090cba 100644 --- a/Fitness/Frontend/src/_nav.js +++ b/Fitness/Frontend/src/_nav.js @@ -9,9 +9,21 @@ export function generateTrainerNav(id) { }, { component: 'CNavItem', - name: 'Schedule', - to: `/trainer/${id}/schedule`, - icon: 'cil-calendar' + name: 'Group training', + to: `/trainer/${id}/groupTrainings`, + icon: 'cil-people', + }, + { + component: 'CNavItem', + name: 'Individual training', + to: `/trainer/${id}/individualTrainings`, + icon: 'cil-people', + }, + { + component: 'CNavItem', + name: 'Nutrition Plan', + to: `/trainer/${id}/nutrition-plan`, + icon: 'cil-pencil', }, { component: 'CNavItem', @@ -19,6 +31,20 @@ export function generateTrainerNav(id) { to: `/trainer/${id}/chat`, icon: 'cil-speech', }, + + { + component: 'CNavItem', + name: 'Video trainings', + to: `/trainer/${id}/videotrainings`, + icon: 'cil-media-play' + }, + + { + component: 'CNavItem', + name: 'Analytics', + to: `/trainer/${id}/analytics`, + icon: 'cilBarChart' + }, ]; } @@ -32,9 +58,21 @@ export function generateClientNav(id) { }, { component: 'CNavItem', - name: 'Schedule', - to: `/client/${id}/schedule`, - icon: 'cil-calendar' + name: 'Group training', + to: `/client/${id}/groupTrainings`, + icon: 'cil-people', + }, + { + component: 'CNavItem', + name: 'Individual training', + to: `/client/${id}/individualTrainings`, + icon: 'cil-user-follow', + }, + { + component: 'CNavItem', + name: 'Nutrition Plan', + to: `/client/${id}/nutrition-plan`, + icon: 'cil-pencil', }, { component: 'CNavItem', @@ -42,6 +80,18 @@ export function generateClientNav(id) { to: `/client/${id}/chat`, icon: 'cil-speech' }, + { + component: 'CNavItem', + name: 'Video trainings', + to: `/client/${id}/videotrainings`, + icon: 'cil-media-play' + }, + { + component: 'CNavItem', + name: 'Analytics', + to: `/client/${id}/analytics`, + icon: 'cilBarChart' + }, ]; } diff --git a/Fitness/Frontend/src/assets/icons/index.js b/Fitness/Frontend/src/assets/icons/index.js index f363459..6617284 100644 --- a/Fitness/Frontend/src/assets/icons/index.js +++ b/Fitness/Frontend/src/assets/icons/index.js @@ -80,6 +80,7 @@ import { cilTrash, cilToggleOn, cilToggleOff, + cilBarChart } from '@coreui/icons' @@ -140,7 +141,8 @@ export const iconsSet = Object.assign( cilXCircle, cilTrash, cilToggleOn, - cilToggleOff + cilToggleOff, + cilBarChart }, { cifUs, diff --git a/Fitness/Frontend/src/assets/images/dumbbells.jpg b/Fitness/Frontend/src/assets/images/dumbbells.jpg new file mode 100644 index 0000000..9b739c0 Binary files /dev/null and b/Fitness/Frontend/src/assets/images/dumbbells.jpg differ diff --git a/Fitness/Frontend/src/assets/images/fitplusplus.jpeg b/Fitness/Frontend/src/assets/images/fitplusplus.jpeg new file mode 100644 index 0000000..fb1ab1b Binary files /dev/null and b/Fitness/Frontend/src/assets/images/fitplusplus.jpeg differ diff --git a/Fitness/Frontend/src/assets/images/home.jpeg b/Fitness/Frontend/src/assets/images/home.jpeg new file mode 100644 index 0000000..86236d0 Binary files /dev/null and b/Fitness/Frontend/src/assets/images/home.jpeg differ diff --git a/Fitness/Frontend/src/assets/images/mix.jpg b/Fitness/Frontend/src/assets/images/mix.jpg new file mode 100644 index 0000000..bde9881 Binary files /dev/null and b/Fitness/Frontend/src/assets/images/mix.jpg differ diff --git a/Fitness/Frontend/src/assets/images/running.jpeg b/Fitness/Frontend/src/assets/images/running.jpeg new file mode 100644 index 0000000..f9850de Binary files /dev/null and b/Fitness/Frontend/src/assets/images/running.jpeg differ diff --git a/Fitness/Frontend/src/assets/images/strength.jpg b/Fitness/Frontend/src/assets/images/strength.jpg new file mode 100644 index 0000000..ea340cd Binary files /dev/null and b/Fitness/Frontend/src/assets/images/strength.jpg differ diff --git a/Fitness/Frontend/src/assets/images/yoga.jpg b/Fitness/Frontend/src/assets/images/yoga.jpg new file mode 100644 index 0000000..d3cd141 Binary files /dev/null and b/Fitness/Frontend/src/assets/images/yoga.jpg differ diff --git a/Fitness/Frontend/src/components/AppHeader.vue b/Fitness/Frontend/src/components/AppHeader.vue index 86c247f..15c443a 100644 --- a/Fitness/Frontend/src/components/AppHeader.vue +++ b/Fitness/Frontend/src/components/AppHeader.vue @@ -4,24 +4,9 @@ - - - - - - - - - - - - - - - - - + + @@ -30,14 +15,13 @@ diff --git a/Fitness/Frontend/src/components/AppHeaderDropdownNotif.vue b/Fitness/Frontend/src/components/AppHeaderDropdownNotif.vue new file mode 100644 index 0000000..d961aa6 --- /dev/null +++ b/Fitness/Frontend/src/components/AppHeaderDropdownNotif.vue @@ -0,0 +1,116 @@ + + + diff --git a/Fitness/Frontend/src/components/ClientModal.vue b/Fitness/Frontend/src/components/ClientModal.vue deleted file mode 100644 index bdeb896..0000000 --- a/Fitness/Frontend/src/components/ClientModal.vue +++ /dev/null @@ -1,244 +0,0 @@ - - - - - diff --git a/Fitness/Frontend/src/main.js b/Fitness/Frontend/src/main.js index 8943dad..d23bba2 100644 --- a/Fitness/Frontend/src/main.js +++ b/Fitness/Frontend/src/main.js @@ -10,7 +10,7 @@ import { iconsSet as icons } from '@/assets/icons' import {LoadingPlugin} from 'vue-loading-overlay'; import 'vue-loading-overlay/dist/css/index.css'; -import axios from 'axios' +import axios from 'axios' axios.interceptors.response.use(response => { return response; diff --git a/Fitness/Frontend/src/router/index.js b/Fitness/Frontend/src/router/index.js index e546956..9cc7e46 100644 --- a/Fitness/Frontend/src/router/index.js +++ b/Fitness/Frontend/src/router/index.js @@ -46,23 +46,28 @@ const routes = [ component: () => import('@/views/pages/Client.vue'), }, + { + path: '/client/:id/groupTrainings', + name: 'Client Group Trainings', + component: () => import('@/views/pages/ClientGroupTrainings.vue') + }, { - path: '/client/:id/schedule', - name: 'Client Schedule', - component: () => import('@/views/pages/ClientSchedule.vue') + path: '/trainer/:id/groupTrainings', + name: 'Trainer Group Trainings', + component: () => import('@/views/pages/TrainerGroupTrainings.vue') }, { - path: '/client/:id/schedule/:trainerId', - name: 'Book Training', - component: () => import('@/views/pages/ClientSchedule.vue') + path: '/client/:id/individualTrainings', + name: 'Client Individual Trainings', + component: () => import('@/views/pages/ClientIndividualTrainings.vue') }, { - path: '/trainer/:id/schedule', - name: 'Trainer Schedule', - component: () => import('@/views/pages/TrainerSchedule.vue') + path: '/trainer/:id/individualTrainings', + name: 'Trainer Individual Trainings', + component: () => import('@/views/pages/TrainerIndividualTrainings.vue') }, { @@ -88,13 +93,47 @@ const routes = [ name: 'Client Chat', component: () => import('@/views/pages/ClientChat.vue'), }, + + { + path: '/client/:id/nutrition-plan', + name: 'Client Nutrition Plan', + component: () => import('@/views/pages/ClientNutritionPlan.vue'), + }, + + { + path: '/trainer/:id/nutrition-plan', + name: 'Trainer Nutrition Plan', + component: () => import('@/views/pages/TrainerNutritionPlan.vue'), + }, + + + { + path: '/client/:id/videotrainings', + name: 'Video Trainings Client', + component: () => import('@/views/pages/VideoTrainingsClient.vue') + }, + + { + path: '/trainer/:id/videotrainings', + name: 'Video Trainings Trainer', + component: () => import('@/views/pages/VideoTrainingsTrainer.vue') + }, { path: '/client/:id/pay-chat/:trainerId', name: 'PayChat', component: () => import('@/views/pages/PayChat.vue'), }, - + { + path: '/client/:id/analytics', + name: 'ClientAnalytics', + component: () => import('@/views/pages/ClientAnalytics.vue') + }, + { + path: '/trainer/:id/analytics', + name: 'TrainerAnalytics', + component: () => import('@/views/pages/TrainerAnalytics.vue') + } ], }, { @@ -140,7 +179,7 @@ router.beforeEach(async (to, from) => { var token = sessionStorage.getItem('accessToken'); if ((token && token != 'null') || to.path == '/login' || to.path == '/registration') { - + return true; } else { diff --git a/Fitness/Frontend/src/services/AnalyticsService.js b/Fitness/Frontend/src/services/AnalyticsService.js new file mode 100644 index 0000000..15a4706 --- /dev/null +++ b/Fitness/Frontend/src/services/AnalyticsService.js @@ -0,0 +1,33 @@ +import axios from "axios"; + +const GATEWAY_URL = "http://localhost:8005"; +const ANALYTICS_URL = `${GATEWAY_URL}/analytics` +// const ANALYTICS_URL = "http://localhost:8018/api/v1/Analytics"; + +const analyticsService = { + async getTrainerIndividualTrainings(trainerId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + let response = await axios.get(`${ANALYTICS_URL}/individual/trainer/${trainerId}`); + return response; + }, + + async getClientIndividualTrainings(clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + let response = await axios.get(`${ANALYTICS_URL}/individual/client/${clientId}`); + return response; + }, + + async getTrainerGroupTrainings(trainerId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + let response = await axios.get(`${ANALYTICS_URL}/group/trainer/${trainerId}`); + return response; + }, + + async getClientGroupTrainings(clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + let response = await axios.get(`${ANALYTICS_URL}/group/client/${clientId}`); + return response; + } +} + +export default analyticsService; diff --git a/Fitness/Frontend/src/services/ChatService.js b/Fitness/Frontend/src/services/ChatService.js index 126ae23..e0653a1 100644 --- a/Fitness/Frontend/src/services/ChatService.js +++ b/Fitness/Frontend/src/services/ChatService.js @@ -2,7 +2,7 @@ import axios from "axios"; const GATEWAY_URL = "http://localhost:8005"; -//const CHAT = "http://localhost:8082/api/Chat"; +//const CHAT = "http://localhost:8082/api/v1/Chat"; //const CLIENT = "http://localhost:8100/api/v1/Client"; //const TRAINERS = "http://localhost:8000/api/v1/Trainer"; @@ -13,6 +13,7 @@ const TRAINERS = `${GATEWAY_URL}/trainer`; export async function getBasicInfoForTrainerSessions(trainerId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.get(`${CHAT}/sessions/${trainerId}/my-sessions-summary`); return response.data; } catch (error) { @@ -23,6 +24,7 @@ export async function getBasicInfoForTrainerSessions(trainerId) { export async function getBasicInfoForClientSessions(clientId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.get(`${CHAT}/sessions/${clientId}/my-sessions-summary`); return response.data; } catch (error) { @@ -33,6 +35,7 @@ export async function getBasicInfoForClientSessions(clientId) { export async function getMessagesFromSession(trainerId, clientId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.get(`${CHAT}/sessions/messages?trainerId=${trainerId}&clientId=${clientId}`); return response.data; } catch (error) { @@ -43,6 +46,7 @@ export async function getMessagesFromSession(trainerId, clientId) { export async function sendMessageToSession(trainerId, clientId, content, senderType) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.post( `${CHAT}/sessions/messages`, content, @@ -66,6 +70,7 @@ export async function sendMessageToSession(trainerId, clientId, content, senderT export async function createChatSession(trainerId, clientId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.post( `${CHAT}/sessions`, null, @@ -82,14 +87,27 @@ export async function createChatSession(trainerId, clientId) { return response; } catch (error) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; console.error("Error creating chat session:", error); alert("Failed to create chat session. Please try again."); throw error; } } +export async function getChatSession(trainerId, clientId) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${CHAT}/sessions?trainerId=${trainerId}&clientId=${clientId}`); + return response; + } catch (error) { + console.error("Error fetching chat session:", error); + throw error; + } +} + export async function extendChatSession(trainerId, clientId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.post( `${CHAT}/sessions/extend`, null, @@ -114,6 +132,7 @@ export async function extendChatSession(trainerId, clientId) { export async function getClientById(clientId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.get(`${CLIENT}/${clientId}`); return response.data; } catch (error) { @@ -124,6 +143,7 @@ export async function getClientById(clientId) { export async function getTrainerById(trainerId) { try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; const response = await axios.get(`${TRAINERS}/${trainerId}`); return response.data; } catch (error) { diff --git a/Fitness/Frontend/src/services/NotificationService.js b/Fitness/Frontend/src/services/NotificationService.js new file mode 100644 index 0000000..bc44dc8 --- /dev/null +++ b/Fitness/Frontend/src/services/NotificationService.js @@ -0,0 +1,103 @@ +import axios from "axios"; + +// const NOTIFICATIONS = "http://localhost:8004/api/v1/Notification"; + +const GATEWAY_URL = "http://localhost:8005"; +const NOTIFICATIONS = `${GATEWAY_URL}/notification`; + +// Admin +export async function getNotifications() { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${NOTIFICATIONS}`); + return response.data; + } catch (error) { + console.error("Error fetching notifications:", error); + throw error; + } +} + +export async function getNotificationsByUserId(userId) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${NOTIFICATIONS}/user/${userId}`); + return response.data; + } catch (error) { + console.error("Error fetching notifications by userId:", error); + throw error; + } +} + +export async function getNotificationById(id) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${NOTIFICATIONS}/${id}`); + return response.data; + } catch (error) { + console.error("Error fetching notification by id:", error); + throw error; + } +} + +export async function updateNotification(notification) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.put(`${NOTIFICATIONS}`, notification, { + headers: { "Content-Type": "application/json" } + }); + console.log(response.data); + return response.data; + } catch (error) { + console.error("Error updating notification:", error); + throw error; + } +} + +export async function markNotificationAsRead(notificationId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.put( + `${NOTIFICATIONS}/${notificationId}/read`, + {}, + { headers: { "Content-Type": "application/json" } } + ) + .then(res => res.data) + .catch(err => { + console.error("Error marking notification as read:", err); + throw err; + }); +} + + +export async function deleteAllNotifications() { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.delete(`${NOTIFICATIONS}`); + return response.data; + } catch (error) { + console.error("Error deleting all notifications:", error); + throw error; + } +} + +export async function deleteNotificationsByUserId(userId) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.delete(`/user/${userId}`); + return response.data; + } catch (error) { + console.error("Error deleting notifications by userId:", error); + throw error; + } +} + +export async function deleteNotificationById(id) { + try { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.delete(`${NOTIFICATIONS}/${id}`); + return response.data; + } catch (error) { + console.error("Error deleting notification by id:", error); + throw error; + } +} diff --git a/Fitness/Frontend/src/services/NutritionService.js b/Fitness/Frontend/src/services/NutritionService.js new file mode 100644 index 0000000..89330b6 --- /dev/null +++ b/Fitness/Frontend/src/services/NutritionService.js @@ -0,0 +1,99 @@ +import axios from "axios"; + +const GATEWAY_URL = "http://localhost:8005"; +const FOOD_URL = `${GATEWAY_URL}/food` +const MEAL_PLANS_URL = `${GATEWAY_URL}/mealplans` +const GOALS_URL = `${GATEWAY_URL}/goals` + +// const BASE_URL = "http://localhost:8157/api/v1"; + +export async function addFood(food) { + try { + const res = await axios.post(`${FOOD_URL}`, food); + return res.data; + } catch (err) { + console.error("Error adding food:", err); + throw err; + } +} + +export async function getAllFoods() { + try { + const res = await axios.get(`${FOOD_URL}`); + return res.data; + } catch (err) { + console.error("Error fetching foods:", err); + throw err; + } +} + + +//trener + +export async function getMealPlansForTrainer(trainerId) { + try { + const res = await axios.get(`${MEAL_PLANS_URL}/trainer/${trainerId}`); + return res.data || []; + } catch (err) { + console.error("Error fetching trainer meal plans:", err); + throw err; + } +} + +export async function saveMealPlan(plan) { + try { + const res = await axios.post(`${MEAL_PLANS_URL}`, plan); + return res.data; + } catch (err) { + console.error("Error saving meal plan:", err); + throw err; + } +} + +export async function deleteMealPlan(trainerId, goalType) { + try { + const res = await axios.delete(`${MEAL_PLANS_URL}/trainer/${trainerId}/goal/${goalType}`); + return res.data; + } catch (err) { + console.error("Error deleting meal plan:", err); + throw err; + } +} + +//klijent + +export async function loadTrainers() { + try { + const res = await axios.get(`${MEAL_PLANS_URL}`); + const seen = new Map(); + for (const p of res.data) { + if (!seen.has(p.trainerId)) { + seen.set(p.trainerId, { id: p.trainerId, name: p.trainerName }); + } + } + return Array.from(seen.values()); + } catch (err) { + console.error("Error loading trainers:", err); + throw err; + } +} + +export async function calculateGoal(goal) { + try { + const res = await axios.post(`${GOALS_URL}`, goal); + return res.data; + } catch (error) { + console.error("Error calculating goal:", error); + throw error; + } +} + +export async function fetchPlan(trainerId, goalType) { + try { + const res = await axios.get(`${MEAL_PLANS_URL}/trainer/${trainerId}/goal/${goalType}`); + return res.data; + } catch (error) { + console.error("Error fetching plan:", error); + throw error; + } +} diff --git a/Fitness/Frontend/src/services/ReservationService.js b/Fitness/Frontend/src/services/ReservationService.js new file mode 100644 index 0000000..d89dea2 --- /dev/null +++ b/Fitness/Frontend/src/services/ReservationService.js @@ -0,0 +1,125 @@ +import axios from "axios"; + +const GATEWAY_URL = "http://localhost:8005"; +const RESERVATIONS = `${GATEWAY_URL}/reservation`; + +// const RESERVATIONS = "http://localhost:8103/api/v1/Reservation"; + +// ---------------------- INDIVIDUAL RESERVATIONS ---------------------- + +// Admin - get all individual +export async function getAllIndividualReservations() { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/individual`); + return response; +} + +// Admin - get individual by id +export async function getIndividualReservationById(id) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/individual/${id}`); + return response; +} + +// Client - get individual by clientId +export async function getIndividualReservationsByClient(clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/individual/client/${clientId}`); + return response; +} + +// Trainer - get individual by trainerId +export async function getIndividualReservationsByTrainer(trainerId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/individual/trainer/${trainerId}`); + return response; +} + +// Client - create individual reservation +export async function createIndividualReservation(reservation) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.post(`${RESERVATIONS}/individual`, reservation, { + headers: { "Content-Type": "application/json" }, + }); + return response; +} + +// ---------------------- GROUP RESERVATIONS ---------------------- + +// Admin - get all group +export async function getAllGroupReservations() { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/group`); + return response; +} + +// Admin - get group by id +export async function getGroupReservationById(id) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/group/${id}`); + return response; +} + +// Client - get group by clientId +export async function getGroupReservationsByClient(clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/group/client/${clientId}`); + return response; +} + +// Trainer - get group by trainerId +export async function getGroupReservationsByTrainer(trainerId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.get(`${RESERVATIONS}/group/trainer/${trainerId}`); + return response; +} + +// Trainer - create group reservation +export async function createGroupReservation(reservation) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.post(`${RESERVATIONS}/group`, reservation, { + headers: { "Content-Type": "application/json" }, + }); + return response; +} + +// Trainer - delete group reservation +export async function deleteGroupReservation(id) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.delete(`${RESERVATIONS}/group/${id}`); + return response; +} + +// ---------------------- GROUP BOOKING / CANCEL ---------------------- + +// Client - book group reservation +export async function bookGroupReservation(id, clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.post(`${RESERVATIONS}/group/book/${id}`, null, { + params: { clientId }, + }); + return response; +} + +// Client - cancel individual reservation +export async function cancelClientIndividualReservation(id) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.put(`${RESERVATIONS}/individual/client/cancel/${id}`); + return response; +} + +// Client - cancel group reservation +export async function cancelGroupReservation(id, clientId) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.post(`${RESERVATIONS}/group/cancel/${id}`, null, { + params: { clientId }, + }); + return response; +} + +// Trainer - cancel individual reservation +export async function cancelTrainerIndividualReservation(id) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + const response = await axios.put(`${RESERVATIONS}/individual/trainer/cancel/${id}`); + return response; +} diff --git a/Fitness/Frontend/src/services/data_services.js b/Fitness/Frontend/src/services/data_services.js index 2788a18..b5433cf 100644 --- a/Fitness/Frontend/src/services/data_services.js +++ b/Fitness/Frontend/src/services/data_services.js @@ -8,7 +8,8 @@ const CLIENT = `${GATEWAY_URL}/client`; const AUTH_URL = `${GATEWAY_URL}/authentication`; const MSSQL_USERS = `${GATEWAY_URL}/user`; const PAYMENT = `${GATEWAY_URL}/payment`; - +const TRAININGS = `${GATEWAY_URL}/training`; +const UPLOAD = `${GATEWAY_URL}/upload` //const TRAINERS = "http://localhost:8000/api/v1/Trainer"; //const REVIEW = "http://localhost:8001/api/v1/Review"; @@ -16,20 +17,25 @@ const PAYMENT = `${GATEWAY_URL}/payment`; //const AUTH_URL = "http://localhost:4000/api/v1/authentication/"; //const MSSQL_USERS = "http://localhost:4000/api/v1/User/"; //const PAYMENT = "http://localhost:8003/api/v1/Payment"; +//const TRAININGS = "http://localhost:8007/api/v1/Training"; +// const UPLOAD = "http://localhost:8007/api/v1/Upload" export default { methods: { + // ==================== + // Authentication + // ==================== login(user, pw) { const data = { username: user, password: pw }; - + const headers = { 'Content-Type': 'application/json' }; - return axios.post(AUTH_URL + '/Login', data, { headers }); + return axios.post(AUTH_URL + '/Login', data, { headers }); }, register(firstname, lastname, username, password, email, phonenumber, role) { @@ -40,7 +46,7 @@ export default { password: password, email: email, phonenumber: phonenumber - }; + }; const headers = { 'Content-Type': 'application/json' @@ -54,26 +60,15 @@ export default { return axios.post(AUTH_URL + '/Logout', request); }, - create_payment(request) { - axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.post(`${PAYMENT}`, request); - }, - - capture_payment(request) { - axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.post(`${PAYMENT}/CapturePayment`, request); - }, - - add_client(request) { - axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.post(`${CLIENT}`, request); - }, - unregister_user(email) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.delete(`${AUTH_URL}${email}`); + var emailEncoded = encodeURIComponent(email) + return axios.delete(`${AUTH_URL}/${emailEncoded}`); }, + // ==================== + // Access Token Handling + // ==================== parse_access_token(access_token) { const at_parts = access_token.split('.'); const payload_string = at_parts[1]; @@ -92,6 +87,22 @@ export default { return at_data[role]; }, + // ==================== + // Payments + // ==================== + create_payment(request) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${PAYMENT}`, request); + }, + + capture_payment(request) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${PAYMENT}/CapturePayment`, request); + }, + + // ==================== + // Users / Clients / Trainers + // ==================== get_user(username) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; return axios.get(MSSQL_USERS + '/' + username); @@ -134,9 +145,9 @@ export default { return axios.put(`${TRAINERS}`, request); }, - upt_client(cl_id, request) { + remove_trainer(tra_id) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.put(`${CLIENT}`, request); + return axios.delete(`${TRAINERS}/${tra_id}`); }, get_trainer_by_id(tra_id) { @@ -154,9 +165,19 @@ export default { return axios.get(`${TRAINERS}/GetTrainersByRating/${rating}`); }, - remove_trainer(tra_id) { + get_price(tra_id, trainingType) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.delete(`${TRAINERS}/${tra_id}`); + return axios.get(`${TRAINERS}/GetPrice/${tra_id}/${trainingType}`); + }, + + add_client(request) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${CLIENT}`, request); + }, + + upt_client(cl_id, request) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.put(`${CLIENT}`, request); }, remove_client(cl_id) { @@ -164,58 +185,110 @@ export default { return axios.delete(`${CLIENT}/${cl_id}`); }, - get_price(tra_id, trainingType) { + get_client_by_id(cli_id) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${TRAINERS}/GetPrice/${tra_id}/${trainingType}`); + return axios.get(`${CLIENT}/${cli_id}`); }, - booking(request) { + // ==================== + // Reviews + // ==================== + get_reviews_client(cli_id) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.put(`${CLIENT}/BookTraining`, request); + return axios.get(`${REVIEW}/client/${cli_id}`); }, - cancelBooking(request) { + get_reviews_trainer(tra_id) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.put(`${TRAINERS}/CancelTraining`, request); + return axios.get(`${REVIEW}/trainer/${tra_id}`); }, - add_review(request) { + submit_review_client(request) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.post(`${REVIEW}`, request); + return axios.post(`${REVIEW}/client/${request.clientId}`, request); }, - get_reviews(tra_id) { + submit_review_trainer(request) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${REVIEW}/${tra_id}`); + return axios.post(`${REVIEW}/trainer/${request.trainerId}`, request); + }, + + create_exercise(exercise) { + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${TRAININGS}/exercise`, exercise); }, - delete_review(rev_id) { + delete_exercise(exercise_id){ axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.delete(`${REVIEW}/${rev_id}`); + return axios.delete(`${TRAININGS}/exercise/${exercise_id}`); }, - update_review(request) { + get_exercises_by_trainer(trainer_id){ axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.put(`${REVIEW}`, request); + return axios.get(`${TRAININGS}/exercises/${trainer_id}`); }, - get_trainer_week_schedule_by_id(week_id, tra_id) { + upload_video(file) { axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${TRAINERS}/GetTrainerWeekSchedule/${tra_id}/${week_id}`); + const formData = new FormData(); + formData.append("file", file); + const response = axios.post(`${UPLOAD}/video`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + return response.data; }, - get_client_week_schedule_by_id(week_id, cl_id) { + delete_video(fileName){ axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${CLIENT}/GetClientWeekSchedule/${cl_id}/${week_id}`); + const response = axios.delete(`${UPLOAD}/video/delete/${fileName}`); }, - get_client_by_id(cli_id) { + create_training(training){ axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${CLIENT}/${cli_id}`); + console.log(JSON.stringify(training)) + return axios.post(`${TRAININGS}/training`, training); + }, + + delete_training(training_id){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.delete(`${TRAININGS}/training/${training_id}`); }, - get_schedule_trainers_by_client_id(cli_id) { + + create_exercises_for_training(trainingExercise){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${TRAININGS}/trainingExercise`, trainingExercise); + }, + + get_trainings_trainer(trainer_id){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.get(`${TRAININGS}/training/trainingTrainer/${trainer_id}`); + }, + + get_trainings_client(){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.get(`${TRAININGS}/training/trainingClient`); + }, + + get_training_exercises(trainer_id){ axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; - return axios.get(`${CLIENT}/GetTrainerIdsFromClientSchedule/${cli_id}`); + return axios.get(`${TRAININGS}/trainingExercises/${trainer_id}`); }, + + delete_training_exercises(training_id){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.delete(`${TRAININGS}/trainingExercise/${training_id}`); + }, + + get_purchased_trainings(client_id){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.get(`${TRAININGS}/training/byClient/${client_id}`); + }, + + buy_training(training_id, client_id){ + axios.defaults.headers.common = { 'Authorization': `Bearer ${sessionStorage.getItem('accessToken')}` }; + return axios.post(`${TRAININGS}/training/${training_id}/addClient/${client_id}`); + } + } } diff --git a/Fitness/Frontend/src/views/pages/Client.vue b/Fitness/Frontend/src/views/pages/Client.vue index 95a3bdd..b53f854 100644 --- a/Fitness/Frontend/src/views/pages/Client.vue +++ b/Fitness/Frontend/src/views/pages/Client.vue @@ -46,12 +46,6 @@
- - View reviews - - - Book training - Pay chat @@ -60,19 +54,14 @@ - -
+ + + diff --git a/Fitness/Frontend/src/views/pages/ClientGroupTrainings.vue b/Fitness/Frontend/src/views/pages/ClientGroupTrainings.vue new file mode 100644 index 0000000..fc0e050 --- /dev/null +++ b/Fitness/Frontend/src/views/pages/ClientGroupTrainings.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/Fitness/Frontend/src/views/pages/ClientIndividualTrainings.vue b/Fitness/Frontend/src/views/pages/ClientIndividualTrainings.vue new file mode 100644 index 0000000..ce7074a --- /dev/null +++ b/Fitness/Frontend/src/views/pages/ClientIndividualTrainings.vue @@ -0,0 +1,628 @@ + + + + + diff --git a/Fitness/Frontend/src/views/pages/ClientNutritionPlan.vue b/Fitness/Frontend/src/views/pages/ClientNutritionPlan.vue new file mode 100644 index 0000000..b6ded7a --- /dev/null +++ b/Fitness/Frontend/src/views/pages/ClientNutritionPlan.vue @@ -0,0 +1,250 @@ + + + + + + diff --git a/Fitness/Frontend/src/views/pages/ClientSchedule.vue b/Fitness/Frontend/src/views/pages/ClientSchedule.vue deleted file mode 100644 index e773d69..0000000 --- a/Fitness/Frontend/src/views/pages/ClientSchedule.vue +++ /dev/null @@ -1,455 +0,0 @@ - - - - - - - - - diff --git a/Fitness/Frontend/src/views/pages/Login.vue b/Fitness/Frontend/src/views/pages/Login.vue index 889613c..bac21bf 100644 --- a/Fitness/Frontend/src/views/pages/Login.vue +++ b/Fitness/Frontend/src/views/pages/Login.vue @@ -86,9 +86,12 @@ export default { dataServices.methods.get_user(this.username) .then( (response) => { + console.log("ovo je 1. response koji smo dobili: ", response); this.email = response.data.email; + console.log("Ovo je mejl koji nas zanima: ", this.email); dataServices.methods.get_user_id(role, this.email) .then( (response) => { + console.log("ovo je 2. response koji smo dobili: ", response); sessionStorage.setItem('userId', response.data.id); const id = response.data.id; if(role == 'Trainer') { @@ -96,6 +99,7 @@ export default { } else { this.$router.push('/client/' + id); + console.log("ovo je Id koji zelimo: " + id); } loader.hide(); diff --git a/Fitness/Frontend/src/views/pages/PayChat.vue b/Fitness/Frontend/src/views/pages/PayChat.vue index 89f78db..14b42ce 100644 --- a/Fitness/Frontend/src/views/pages/PayChat.vue +++ b/Fitness/Frontend/src/views/pages/PayChat.vue @@ -28,22 +28,21 @@

Secure your 30-day chat mentorship now!

- - + diff --git a/Fitness/Frontend/src/views/pages/PaymentSuccess.vue b/Fitness/Frontend/src/views/pages/PaymentSuccess.vue index 7b85640..e466df8 100644 --- a/Fitness/Frontend/src/views/pages/PaymentSuccess.vue +++ b/Fitness/Frontend/src/views/pages/PaymentSuccess.vue @@ -9,58 +9,159 @@ diff --git a/Fitness/Frontend/src/views/pages/TrainerGroupTrainings.vue b/Fitness/Frontend/src/views/pages/TrainerGroupTrainings.vue new file mode 100644 index 0000000..1877934 --- /dev/null +++ b/Fitness/Frontend/src/views/pages/TrainerGroupTrainings.vue @@ -0,0 +1,532 @@ + + + + + + diff --git a/Fitness/Frontend/src/views/pages/TrainerIndividualTrainings.vue b/Fitness/Frontend/src/views/pages/TrainerIndividualTrainings.vue new file mode 100644 index 0000000..8b6022c --- /dev/null +++ b/Fitness/Frontend/src/views/pages/TrainerIndividualTrainings.vue @@ -0,0 +1,456 @@ + + + + + diff --git a/Fitness/Frontend/src/views/pages/TrainerNutritionPlan.vue b/Fitness/Frontend/src/views/pages/TrainerNutritionPlan.vue new file mode 100644 index 0000000..0bbabae --- /dev/null +++ b/Fitness/Frontend/src/views/pages/TrainerNutritionPlan.vue @@ -0,0 +1,220 @@ + + + + + + diff --git a/Fitness/Frontend/src/views/pages/TrainerSchedule.vue b/Fitness/Frontend/src/views/pages/TrainerSchedule.vue deleted file mode 100644 index 630dfb2..0000000 --- a/Fitness/Frontend/src/views/pages/TrainerSchedule.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - - diff --git a/Fitness/Frontend/src/views/pages/VideoTrainingsClient.vue b/Fitness/Frontend/src/views/pages/VideoTrainingsClient.vue new file mode 100644 index 0000000..1c6219d --- /dev/null +++ b/Fitness/Frontend/src/views/pages/VideoTrainingsClient.vue @@ -0,0 +1,465 @@ + + + + + diff --git a/Fitness/Frontend/src/views/pages/VideoTrainingsTrainer.vue b/Fitness/Frontend/src/views/pages/VideoTrainingsTrainer.vue new file mode 100644 index 0000000..9f86940 --- /dev/null +++ b/Fitness/Frontend/src/views/pages/VideoTrainingsTrainer.vue @@ -0,0 +1,710 @@ + + + + + diff --git a/README.md b/README.md index 3b604f8..5d03ecd 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ This project is an **extension of the previous FitPlusPlus application**, which ### Current Development Team (2024): -1. **Aleksandra Labović** – Student ID: 1025/2024 -2. **Vukašin Marković** – Student ID: 1051/2024 -3. **Stefan Milenković** – Student ID: 1076/2024 -4. **Milan Mitreski** – Student ID: 1073/2024 -5. **Natalija Filipović** – Student ID: 1013/2024 +1. **Marković Vukašin** – Student ID: 1051/2024 +2. **Milenković Stefan** – Student ID: 1076/2024; +3. **Mitreski Milan** – Student ID: 1073/2024 +4. **Filipović Natalija** – Student ID: 1013/2024 ### Previous Development Team (2023): -1. **Lazar Stanojević** – Student ID: 1013/2023 -2. **Vasilije Todorović** – Student ID: 1015/2023 -3. **Nikola Belaković** – Student ID: 1023/2023 +3. **Belaković Nikola** – Student ID: 1023/2023 +1. **Stanojević Lazar** – Student ID: 1013/2023 +2. **Todorović Vasilije** – Student ID: 1015/2023 GitHub Repository of the Previous Project: [FitPlusPlus](https://github.com/lazars01/FitPlusPlus) @@ -77,19 +76,15 @@ The FitPlusPlus application consists of multiple microservices, some developed b - Enables **booking of individual and group training sessions**. - Supports **real-time scheduling, cancellation, and availability tracking**. -4. **ManagerService** - - A service for **gym administrators**, providing tools for managing trainers, clients, and finances. - - Generates **performance reports and financial summaries**. - -5. **NotificationService** +4. **NotificationService** - Sends **push and email notifications** to clients, trainers, and administrators. - Includes **reservation confirmations, training reminders, and membership renewal alerts**. -6. **AnalyticsService** +5. **AnalyticsService** - Creates **detailed statistics** on training sessions, client engagement, and trainer performance. - Provides insights into **popular training types and user activity trends**. -7. **Gateway and Discovery Service** +6. **Gateway and Discovery Service** - A **centralized API gateway** that directs requests to the correct microservice. - Facilitates **automatic detection and scaling** of microservices. diff --git a/assets/admin.png b/assets/admin.png deleted file mode 100644 index e7aed6e..0000000 Binary files a/assets/admin.png and /dev/null differ diff --git a/assets/izbortrenera.png b/assets/izbortrenera.png deleted file mode 100644 index 2f2a8ea..0000000 Binary files a/assets/izbortrenera.png and /dev/null differ diff --git a/assets/login.png b/assets/login.png deleted file mode 100644 index 84f6f4e..0000000 Binary files a/assets/login.png and /dev/null differ diff --git a/assets/registracija.png b/assets/registracija.png deleted file mode 100644 index 0082bae..0000000 Binary files a/assets/registracija.png and /dev/null differ diff --git a/assets/trener.png b/assets/trener.png deleted file mode 100644 index c913496..0000000 Binary files a/assets/trener.png and /dev/null differ diff --git a/assets/zakazivanje.png b/assets/zakazivanje.png deleted file mode 100644 index 6e5fd75..0000000 Binary files a/assets/zakazivanje.png and /dev/null differ