From b6778046c2baa40ca3ae947e1b82ec940262d6a7 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Sat, 20 Sep 2025 22:33:35 +0300 Subject: [PATCH] initial commit from an older template --- .gitignore | 650 ++++++++++++++++++ Controllers/AuthController.cs | 486 +++++++++++++ Controllers/InstructionController.cs | 166 +++++ Controllers/InstructionTestController.cs | 278 ++++++++ Controllers/RightsController.cs | 186 +++++ Controllers/RoleController.cs | 283 ++++++++ Controllers/UserProfileController.cs | 184 +++++ Database/ApplicationContext.cs | 50 ++ .../Extensions/ChangeTrackerExtensions.cs | 54 ++ Database/Repositories/GenericRepository.cs | 98 +++ Database/Repositories/UnitOfWork.cs | 239 +++++++ Dockerfile | 23 + .../AuthService/AuthServiceException.cs | 26 + .../Instruction/CategoryNotFoundException.cs | 25 + .../Instruction/InstructionAccessException.cs | 24 + .../InstructionCreationException.cs | 24 + .../InstructionDeletionException.cs | 25 + .../InstructionNotFoundException.cs | 25 + .../Instruction/InstructionUpdateException.cs | 25 + .../InstructionTestConflictException.cs | 26 + .../InstructionTestCreationException.cs | 26 + .../InstructionTestDeletionException.cs | 25 + .../InstructionTestNotFoundException.cs | 27 + .../InstructionTestSubmissionException.cs | 27 + .../InstructionTestUpdateException.cs | 0 .../GenerateRefreshTokenException.cs | 26 + .../JwtService/JwtServiceException.cs | 26 + .../Services/User/UserNotFoundException.cs | 25 + .../Services/User/UserServiceException.cs | 25 + .../ProfileCreationException.cs | 26 + .../ProfileDeletionException.cs | 26 + .../ProfileExistsException.cs | 26 + .../ProfileNotFoundException.cs | 26 + .../ProfileUpdateException.cs | 26 + Exceptions/UtilServices/Api/ApiException.cs | 26 + .../UtilServices/Api/BadRequestException.cs | 25 + .../UtilServices/Api/ForbiddenException.cs | 26 + .../UtilServices/Cookies/CookiesException.cs | 26 + .../Cookies/DeleteCookiesException.cs | 26 + .../Cookies/SetCookiesException.cs | 26 + .../UtilServices/Email/EmailException.cs | 25 + .../UtilServices/Email/SendEmailException.cs | 25 + .../JWT/GenerateJWTTokenException.cs | 25 + Exceptions/UtilServices/JWT/JWTException.cs | 25 + Extensions/DependencyInjectionExtensions.cs | 204 ++++++ MDSBackend.csproj | 53 ++ Mapper/MappingProfile.cs | 169 +++++ Models/BasicResponses/BasicResponse.cs | 7 + Models/DTO/AuthDTO.cs | 21 + Models/DTO/DisableTwoFactorDTO.cs | 7 + Models/DTO/EnableTwoFactorDTO.cs | 6 + Models/DTO/GetAllRightsResponse.cs | 11 + Models/DTO/GetAllRolesResponse.cs | 11 + Models/DTO/GetTwoFactorDTO.cs | 11 + Models/DTO/InstructionCategoryCreateDTO.cs | 14 + Models/DTO/InstructionCategoryDTO.cs | 10 + Models/DTO/InstructionCreateDTO.cs | 38 + Models/DTO/InstructionDTO.cs | 21 + Models/DTO/InstructionParagraphCreateDTO.cs | 25 + Models/DTO/InstructionParagraphDTO.cs | 21 + Models/DTO/InstructionTestCreateDTO.cs | 20 + Models/DTO/InstructionTestDTO.cs | 23 + .../DTO/InstructionTestQuestionCreateDTO.cs | 26 + Models/DTO/InstructionTestQuestionDTO.cs | 25 + Models/DTO/InstructionTestResultDTO.cs | 12 + Models/DTO/InstructionTestSubmissionDTO.cs | 12 + Models/DTO/LoginResultResponse.cs | 9 + Models/DTO/RefreshTokenDTO.cs | 7 + Models/DTO/RightDTO.cs | 7 + Models/DTO/RoleDTO.cs | 7 + Models/DTO/TwoFactorDTO.cs | 16 + Models/DTO/UserProfileCreateDTO.cs | 33 + Models/DTO/UserProfileDTO.cs | 37 + Models/Database/ApplicationRole.cs | 15 + Models/Database/ApplicationUser.cs | 19 + Models/Database/AuditableEntity.cs | 9 + Models/Database/Instruction.cs | 29 + Models/Database/InstructionCategory.cs | 13 + Models/Database/InstructionParagraph.cs | 21 + Models/Database/InstructionTest.cs | 23 + Models/Database/InstructionTestQuestion.cs | 26 + Models/Database/InstructionTestResult.cs | 20 + Models/Database/RefreshToken.cs | 25 + Models/Database/Right.cs | 18 + Models/Database/RoleRight.cs | 10 + Models/Database/UserProfile.cs | 40 ++ Models/Database/UserRole.cs | 10 + .../CreateInstructionTestRequest.cs | 6 + .../CreateInstructionTestResponse.cs | 6 + .../UpdateInstructionTestRequest.cs | 6 + .../UpdateInstructionTestResponse.cs | 6 + .../Instructions/CreateInstructionRequest.cs | 37 + .../Instructions/CreateInstructionResponse.cs | 6 + .../Instructions/UpdateInstructionRequest.cs | 40 ++ .../Instructions/UpdateInstructionResponse.cs | 6 + .../UserProfiles/CreateUserProfileRequest.cs | 6 + .../UserProfiles/CreateUserProfileResponse.cs | 7 + .../UserProfiles/UpdateUserProfileRequest.cs | 6 + .../UserProfiles/UpdateUserProfileResponse.cs | 6 + Models/UserSession.cs | 7 + Program.cs | 83 +++ Properties/launchSettings.json | 23 + Services/Cookies/CookieService.cs | 47 ++ Services/Cookies/ICookieService.cs | 7 + Services/CurrentUsers/CurrentUserService.cs | 26 + Services/CurrentUsers/ICurrentUserService.cs | 8 + .../IInstructionTestsService.cs | 22 + .../InstructionTestsService.cs | 294 ++++++++ Services/Instructions/IInstructionService.cs | 21 + Services/Instructions/InstructionService.cs | 174 +++++ Services/JWT/IJWTService.cs | 13 + Services/JWT/JWTService.cs | 144 ++++ Services/Notification/INotificationService.cs | 11 + Services/Notification/NotificationService.cs | 56 ++ Services/Rights/IRightsService.cs | 12 + Services/Rights/RightsService.cs | 103 +++ Services/Roles/IRolesService.cs | 14 + Services/Roles/RolesService.cs | 162 +++++ Services/UsersProfile/IUserProfileService.cs | 15 + Services/UsersProfile/UserProfileService.cs | 121 ++++ Utils/Clients/EmailClient.cs | 38 + Utils/Clients/PushNotificationsClient.cs | 150 ++++ Utils/Enums/ClickActionType.cs | 6 + Utils/Enums/Gender.cs | 7 + Utils/Enums/InstructionTestScoreCalcMethod.cs | 8 + Utils/Enums/NotificationInformationType.cs | 9 + Utils/Factory/MailNotificationsFactory.cs | 21 + Utils/Factory/PushNotificationsFactory.cs | 28 + Utils/MailNotification.cs | 111 +++ Utils/Notification.cs | 33 + Utils/PushNotification.cs | 40 ++ Utils/TwoFactorProvider.cs | 10 + appsettings.Development.template.json | 33 + appsettings.template.json | 32 + 134 files changed, 6657 insertions(+) create mode 100644 .gitignore create mode 100755 Controllers/AuthController.cs create mode 100644 Controllers/InstructionController.cs create mode 100644 Controllers/InstructionTestController.cs create mode 100644 Controllers/RightsController.cs create mode 100644 Controllers/RoleController.cs create mode 100644 Controllers/UserProfileController.cs create mode 100755 Database/ApplicationContext.cs create mode 100755 Database/Extensions/ChangeTrackerExtensions.cs create mode 100755 Database/Repositories/GenericRepository.cs create mode 100755 Database/Repositories/UnitOfWork.cs create mode 100755 Dockerfile create mode 100755 Exceptions/Services/AuthService/AuthServiceException.cs create mode 100644 Exceptions/Services/Instruction/CategoryNotFoundException.cs create mode 100644 Exceptions/Services/Instruction/InstructionAccessException.cs create mode 100644 Exceptions/Services/Instruction/InstructionCreationException.cs create mode 100644 Exceptions/Services/Instruction/InstructionDeletionException.cs create mode 100644 Exceptions/Services/Instruction/InstructionNotFoundException.cs create mode 100644 Exceptions/Services/Instruction/InstructionUpdateException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestConflictException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestCreationException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestDeletionException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestNotFoundException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestSubmissionException.cs create mode 100644 Exceptions/Services/InstructionTest/InstructionTestUpdateException.cs create mode 100755 Exceptions/Services/JwtService/GenerateRefreshTokenException.cs create mode 100755 Exceptions/Services/JwtService/JwtServiceException.cs create mode 100755 Exceptions/Services/User/UserNotFoundException.cs create mode 100755 Exceptions/Services/User/UserServiceException.cs create mode 100644 Exceptions/UserProfileService/ProfileCreationException.cs create mode 100644 Exceptions/UserProfileService/ProfileDeletionException.cs create mode 100644 Exceptions/UserProfileService/ProfileExistsException.cs create mode 100644 Exceptions/UserProfileService/ProfileNotFoundException.cs create mode 100644 Exceptions/UserProfileService/ProfileUpdateException.cs create mode 100755 Exceptions/UtilServices/Api/ApiException.cs create mode 100755 Exceptions/UtilServices/Api/BadRequestException.cs create mode 100755 Exceptions/UtilServices/Api/ForbiddenException.cs create mode 100755 Exceptions/UtilServices/Cookies/CookiesException.cs create mode 100755 Exceptions/UtilServices/Cookies/DeleteCookiesException.cs create mode 100755 Exceptions/UtilServices/Cookies/SetCookiesException.cs create mode 100755 Exceptions/UtilServices/Email/EmailException.cs create mode 100755 Exceptions/UtilServices/Email/SendEmailException.cs create mode 100755 Exceptions/UtilServices/JWT/GenerateJWTTokenException.cs create mode 100755 Exceptions/UtilServices/JWT/JWTException.cs create mode 100755 Extensions/DependencyInjectionExtensions.cs create mode 100755 MDSBackend.csproj create mode 100755 Mapper/MappingProfile.cs create mode 100755 Models/BasicResponses/BasicResponse.cs create mode 100755 Models/DTO/AuthDTO.cs create mode 100644 Models/DTO/DisableTwoFactorDTO.cs create mode 100644 Models/DTO/EnableTwoFactorDTO.cs create mode 100644 Models/DTO/GetAllRightsResponse.cs create mode 100644 Models/DTO/GetAllRolesResponse.cs create mode 100644 Models/DTO/GetTwoFactorDTO.cs create mode 100644 Models/DTO/InstructionCategoryCreateDTO.cs create mode 100644 Models/DTO/InstructionCategoryDTO.cs create mode 100644 Models/DTO/InstructionCreateDTO.cs create mode 100644 Models/DTO/InstructionDTO.cs create mode 100644 Models/DTO/InstructionParagraphCreateDTO.cs create mode 100644 Models/DTO/InstructionParagraphDTO.cs create mode 100644 Models/DTO/InstructionTestCreateDTO.cs create mode 100644 Models/DTO/InstructionTestDTO.cs create mode 100644 Models/DTO/InstructionTestQuestionCreateDTO.cs create mode 100644 Models/DTO/InstructionTestQuestionDTO.cs create mode 100644 Models/DTO/InstructionTestResultDTO.cs create mode 100644 Models/DTO/InstructionTestSubmissionDTO.cs create mode 100755 Models/DTO/LoginResultResponse.cs create mode 100755 Models/DTO/RefreshTokenDTO.cs create mode 100644 Models/DTO/RightDTO.cs create mode 100644 Models/DTO/RoleDTO.cs create mode 100755 Models/DTO/TwoFactorDTO.cs create mode 100644 Models/DTO/UserProfileCreateDTO.cs create mode 100755 Models/DTO/UserProfileDTO.cs create mode 100755 Models/Database/ApplicationRole.cs create mode 100755 Models/Database/ApplicationUser.cs create mode 100755 Models/Database/AuditableEntity.cs create mode 100644 Models/Database/Instruction.cs create mode 100644 Models/Database/InstructionCategory.cs create mode 100644 Models/Database/InstructionParagraph.cs create mode 100644 Models/Database/InstructionTest.cs create mode 100644 Models/Database/InstructionTestQuestion.cs create mode 100644 Models/Database/InstructionTestResult.cs create mode 100755 Models/Database/RefreshToken.cs create mode 100755 Models/Database/Right.cs create mode 100755 Models/Database/RoleRight.cs create mode 100755 Models/Database/UserProfile.cs create mode 100755 Models/Database/UserRole.cs create mode 100644 Models/Messages/InstructionTests/CreateInstructionTestRequest.cs create mode 100644 Models/Messages/InstructionTests/CreateInstructionTestResponse.cs create mode 100644 Models/Messages/InstructionTests/UpdateInstructionTestRequest.cs create mode 100644 Models/Messages/InstructionTests/UpdateInstructionTestResponse.cs create mode 100644 Models/Messages/Instructions/CreateInstructionRequest.cs create mode 100644 Models/Messages/Instructions/CreateInstructionResponse.cs create mode 100644 Models/Messages/Instructions/UpdateInstructionRequest.cs create mode 100644 Models/Messages/Instructions/UpdateInstructionResponse.cs create mode 100644 Models/Messages/UserProfiles/CreateUserProfileRequest.cs create mode 100644 Models/Messages/UserProfiles/CreateUserProfileResponse.cs create mode 100644 Models/Messages/UserProfiles/UpdateUserProfileRequest.cs create mode 100644 Models/Messages/UserProfiles/UpdateUserProfileResponse.cs create mode 100755 Models/UserSession.cs create mode 100755 Program.cs create mode 100755 Properties/launchSettings.json create mode 100755 Services/Cookies/CookieService.cs create mode 100755 Services/Cookies/ICookieService.cs create mode 100644 Services/CurrentUsers/CurrentUserService.cs create mode 100644 Services/CurrentUsers/ICurrentUserService.cs create mode 100644 Services/InstructionTests/IInstructionTestsService.cs create mode 100644 Services/InstructionTests/InstructionTestsService.cs create mode 100644 Services/Instructions/IInstructionService.cs create mode 100644 Services/Instructions/InstructionService.cs create mode 100755 Services/JWT/IJWTService.cs create mode 100755 Services/JWT/JWTService.cs create mode 100755 Services/Notification/INotificationService.cs create mode 100755 Services/Notification/NotificationService.cs create mode 100755 Services/Rights/IRightsService.cs create mode 100755 Services/Rights/RightsService.cs create mode 100755 Services/Roles/IRolesService.cs create mode 100755 Services/Roles/RolesService.cs create mode 100755 Services/UsersProfile/IUserProfileService.cs create mode 100755 Services/UsersProfile/UserProfileService.cs create mode 100755 Utils/Clients/EmailClient.cs create mode 100755 Utils/Clients/PushNotificationsClient.cs create mode 100755 Utils/Enums/ClickActionType.cs create mode 100644 Utils/Enums/Gender.cs create mode 100644 Utils/Enums/InstructionTestScoreCalcMethod.cs create mode 100755 Utils/Enums/NotificationInformationType.cs create mode 100755 Utils/Factory/MailNotificationsFactory.cs create mode 100755 Utils/Factory/PushNotificationsFactory.cs create mode 100755 Utils/MailNotification.cs create mode 100755 Utils/Notification.cs create mode 100755 Utils/PushNotification.cs create mode 100644 Utils/TwoFactorProvider.cs create mode 100755 appsettings.Development.template.json create mode 100755 appsettings.template.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..195ed00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,650 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,vim,aspnetcore,dotnetcore +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,rider,vim,aspnetcore,dotnetcore + +### ASPNETCore ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ + +### DotnetCore ### +# .NET Core build folders +bin/ +obj/ + +# Common node modules locations +/node_modules +/wwwroot/node_modules + +### Rider ### +# 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 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### VisualStudio ### +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files +mono_crash.* + +# Build results +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results + +# NUnit +nunit-*.xml + +# Build Results of an ATL Project + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_h.h +*.iobj +*.ipdb +*_wpftmp.csproj +*.tlog + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.ndf + +# Business Intelligence projects +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,vim,aspnetcore,dotnetcore diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs new file mode 100755 index 0000000..504a4fc --- /dev/null +++ b/Controllers/AuthController.cs @@ -0,0 +1,486 @@ +using GamificationService.Models.BasicResponses; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Services.JWT; +using GamificationService.Services.NotificationService; +using GamificationService.Utils; +using GamificationService.Utils.Factory; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace GamificationService.Controllers; +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + #region Services + + private readonly ILogger _logger; + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IJwtService _jwtService; + private readonly INotificationService _notificationService; + private readonly MailNotificationsFactory _mailNotificationsFactory; + private readonly PushNotificationsFactory _pushNotificationsFactory; + + + #endregion + + #region Constructor + + public AuthController(ILogger logger, UserManager userManager, SignInManager signInManager, IJwtService jwtService, INotificationService notificationService, MailNotificationsFactory mailNotificationsFactory, PushNotificationsFactory pushNotificationsFactory) + { + _logger = logger; + _userManager = userManager; + _signInManager = signInManager; + _jwtService = jwtService; + _notificationService = notificationService; + _mailNotificationsFactory = mailNotificationsFactory; + _pushNotificationsFactory = pushNotificationsFactory; + } + + #endregion + + #region Actions + + #region Auth + + /// + /// Handles user registration. + /// + /// The registration model. + /// A response indicating the result of the registration. + [HttpPost("register")] + public async Task Register([FromBody] AuthDTO model) + { + try + { + var user = new ApplicationUser() + { + UserName = model.Username, + Email = model.Email, + TwoFactorEnabled = false, + TwoFactorProviders = new List() { TwoFactorProvider.NONE } + }; + + var result = await _userManager.CreateAsync(user, model.Password); + + if (!result.Succeeded) + { + _logger.LogError("User registration failed: {Errors}", result.Errors); + return BadRequest(new BasicResponse + { + Code = 400, + Message = "User registration failed" + }); + } + + _logger.LogInformation("User registered successfully: {Username}", model.Username); + return Ok(new BasicResponse + { + Code = 200, + Message = "User created successfully" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during user registration: {Message}", ex.Message); + return StatusCode(500, new BasicResponse + { + Code = 500, + Message = "An error occurred during user registration" + }); + } + } + + + /// + /// Handles user login. + /// + /// The login model. + /// A response indicating the result of the login. + [HttpPost("login")] + public async Task Login([FromBody] AuthDTO model) + { + try + { + var user = await _userManager.FindByNameAsync(model.Username); + + if (user == null) + { + _logger.LogError("Invalid username or password"); + return NotFound(new BasicResponse + { + Code = 404, + Message = "Invalid username or password" + }); + } + var result = await _signInManager.PasswordSignInAsync( user, model.Password, false, model.RememberMe); + + if (result.Succeeded & !result.RequiresTwoFactor) + { + var refreshToken = await _jwtService.GenerateRefreshTokenAsync(user); + var accessToken = _jwtService.GenerateAccessToken(user); + _logger.LogInformation("User logged in successfully: {Username}", model.Username); + + return Ok(new LoginResultResponse() + { + RequiresTwoFactorAuth = false, + Success = true, + Token = new RefreshTokenDTO() + { + AccessToken = accessToken, + RefreshToken = refreshToken.Token + } + }); + } + else if(result.RequiresTwoFactor) + { + var providerWithMaxWeight = user.TwoFactorProviders + .OrderByDescending(p => (int)p) + .FirstOrDefault(); + + if (providerWithMaxWeight == TwoFactorProvider.NONE) + { + _logger.LogInformation("User {Username} does not have any two-factor authentication enabled", model.Username); + return StatusCode(418, new LoginResultResponse() + { + RequiresTwoFactorAuth = false, + Success = true, + TwoFactorProvider = (int)providerWithMaxWeight + }); + } + + var code = await _userManager.GenerateTwoFactorTokenAsync(user, providerWithMaxWeight.ToString()); + await SendNotificationAsync(user, "Two-factor authentication code", code, NotificationInformationType.AUTH,providerWithMaxWeight); + _logger.LogInformation("Two-factor authentication required for user {Username}", model.Username); + return Ok(new LoginResultResponse() + { + RequiresTwoFactorAuth = true, + Success = true + }); + } + + _logger.LogError("Invalid username or password"); + return BadRequest(new BasicResponse + { + Code = 400, + Message = "Invalid username or password" + }); + + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during user login: {Message}", ex.Message); + return StatusCode(500, new BasicResponse + { + Code = 500, + Message = "An error occurred during user login" + }); + } + } + [HttpPost("logout")] + [Authorize] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + return Ok("Logged out successfully"); + } + + [HttpPost("revoke-token")] + [Authorize] + public async Task RevokeToken() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound("User not found"); + } + + await _jwtService.RevokeRefreshTokenAsync(user.Id, HttpContext.Request.Cookies["refresh_token"],GetRemoteIpAddress()); + return Ok("Token revoked successfully"); + } + #endregion + + #region Email + + [HttpGet("{username}/init-email-verification")] + public async Task VerifyEmail(string username) + { + try + { + var user = await _userManager.FindByNameAsync(username); + if (user == null) + { + _logger.LogError("Invalid username or password"); + return NotFound(new BasicResponse + { + Code = 404, + Message = "Invalid username or password" + }); + } + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + await SendNotificationAsync(user, "Email verification code", code, NotificationInformationType.AUTH, TwoFactorProvider.EMAIL); + _logger.LogInformation("Email verification code sent to user {Username}", username); + return Ok(new BasicResponse() + { + Code = 200, + Message = "Email verification code sent" + }); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred during email verification: {Message}", e.Message); + return StatusCode(500, new BasicResponse + { + Code = 500, + Message = "An error occurred during email verification" + }); + } + } + + [HttpGet("{username}/verify-email/{code}")] + public async Task VerifyEmail(string username, string code) + { + try + { + var user = await _userManager.FindByNameAsync(username); + if (user == null) + { + _logger.LogError("Invalid username or password"); + return NotFound(new BasicResponse + { + Code = 404, + Message = "Invalid username or password" + }); + } + + var result = await _userManager.ConfirmEmailAsync(user,code); + if (result.Succeeded) + { + _logger.LogInformation("Email verified for user {Username}", username); + user.EmailConfirmed = true; + await _userManager.UpdateAsync(user); + return Ok(new BasicResponse() + { + Code = 200, + Message = "Email verified" + }); + } + return BadRequest(new BasicResponse() + { + Code = 400, + Message = "Email verification failed" + }); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred during email verification: {Message}", e.Message); + return StatusCode(500, new BasicResponse + { + Code = 500, + Message = "An error occurred during email verification" + }); + } + } + #endregion + + #region 2FA + + [HttpPost("get-2fa-code")] + public async Task GetTwoFactorCode([FromBody] GetTwoFactorDTO model) + { + try + { + var user = await _userManager.FindByNameAsync(model.Username); + + if (user == null) + { + _logger.LogError("Invalid username or password"); + return NotFound(new BasicResponse + { + Code = 404, + Message = "Invalid username or password" + }); + } + + var providerWithRequiredWeight = user.TwoFactorProviders + .FirstOrDefault(p => (int)p == model.TwoFactorProvider); + var code = await _userManager.GenerateTwoFactorTokenAsync(user, providerWithRequiredWeight.ToString()); + await SendNotificationAsync(user, "Two-factor authentication code", code, NotificationInformationType.AUTH,providerWithRequiredWeight); + _logger.LogInformation("Two-factor authentication code sent to user {Username}", model.Username); + + return Ok(new BasicResponse() + { + Code = 200, + Message = "Code sent successfully" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to send code" + }); + } + } + + [HttpPost("verify-2fa")] + public async Task VerifyTwoFactorCode([FromBody] TwoFactorDTO model) + { + try + { + if (model.Username != null) + { + var user = await _userManager.FindByNameAsync(model.Username); + var providerWithRequiredWeight = user.TwoFactorProviders + .FirstOrDefault(p => (int)p == model.TwoFactorProvider); + var signInResult = _signInManager.TwoFactorSignInAsync(providerWithRequiredWeight.ToString(), + model.Code, false, model.RememberMe); + + + if (!signInResult.Result.Succeeded) + { + return BadRequest(new BasicResponse() + { + Code = 400, + Message = "Invalid code" + }); + } + + var token = _jwtService.GenerateAccessToken(user); + var refreshToken = await _jwtService.GenerateRefreshTokenAsync(user); + + _logger.LogInformation("User logged in successfully: {Username}", model.Username); + await SendNotificationAsync(user, "Login successful", "You have successfully logged in", NotificationInformationType.WARNING,TwoFactorProvider.EMAIL); + + return Ok( new LoginResultResponse() + { + RequiresTwoFactorAuth = false, + Success = true, + Token = new RefreshTokenDTO() + { + AccessToken = token, + RefreshToken = refreshToken.Token + } + }); + } + _logger.LogError("Username can't be empty"); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Username can't be empty" + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during user verification: {Message}", ex.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "An error occurred during user verification" + }); + } + } + + [HttpPost("enable-2fa")] + [Authorize] + public async Task EnableTwoFactor([FromBody]EnableTwoFactorDTO model) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound(new BasicResponse() + { + Code = 404, + Message = "User not found" + }); + } + + user.TwoFactorProviders.Add((TwoFactorProvider)model.TwoFactorProvider); + user.TwoFactorEnabled = true; + await _userManager.UpdateAsync(user); + var secretKey = await _userManager.GenerateTwoFactorTokenAsync(user, TwoFactorProvider.AUTHENTICATOR.ToString()); + _logger.LogInformation("User logged in successfully: {Username}", User); + await SendNotificationAsync(user, "Login successful", "You have successfully logged in", NotificationInformationType.WARNING,(TwoFactorProvider)model.TwoFactorProvider); + + return Ok(new BasicResponse() + { + Code = 200, + Message = "Two-factor authentication enabled successfully" + }); + } + + [HttpPost("disable-2fa")] + [Authorize] + public async Task DisableTwoFactor([FromBody] DisableTwoFactorDTO model) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return NotFound("User not found"); + } + + if (!await _userManager.VerifyTwoFactorTokenAsync(user,model.TwoFactorProvider.ToString(),model.Code)) + { + return BadRequest("Invalid verification code"); + } + + user.TwoFactorEnabled = false; + await _userManager.UpdateAsync(user); + + return Ok("Two-factor authentication disabled"); + } + + #endregion + + #region Helpers + + private async Task SendNotificationAsync(ApplicationUser user, + string title, + string message, + NotificationInformationType notificationInformationType, + TwoFactorProvider provider) + { + try + { + switch (provider) + { + case TwoFactorProvider.EMAIL: + await _notificationService.SendMailNotificationAsync(user, _mailNotificationsFactory.CreateNotification(notificationInformationType, title, message)); + break; + case TwoFactorProvider.PHONE: + throw new NotImplementedException(); + break; + case TwoFactorProvider.PUSH: + await _notificationService.SendPushNotificationAsync(user, _pushNotificationsFactory.CreateNotification(notificationInformationType, title, message)); + break; + case TwoFactorProvider.AUTHENTICATOR: + throw new NotImplementedException(); + break; + default: + break; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during notification: {Message}", ex.Message); + } + } + + private string GetRemoteIpAddress() + { + if (HttpContext.Connection.RemoteIpAddress != null) + { + return HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); + } + return string.Empty; + } + #endregion + + #endregion +} diff --git a/Controllers/InstructionController.cs b/Controllers/InstructionController.cs new file mode 100644 index 0000000..c3fa8dc --- /dev/null +++ b/Controllers/InstructionController.cs @@ -0,0 +1,166 @@ +using GamificationService.Exceptions.Services.Instruction; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Models.Messages.Instructions; +using GamificationService.Services.Instructions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace GamificationService.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = "User")] +public class InstructionController : ControllerBase +{ + private readonly IInstructionService _instructionService; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public InstructionController(IInstructionService instructionService, UserManager userManager, ILogger logger) + { + _instructionService = instructionService; + _userManager = userManager; + _logger = logger; + } + + /// + /// Create a new instruction. + /// + /// The instruction model. + /// which was created + /// Returns the created instruction + [HttpPost] + [Authorize(Policy = "Admin")] + public async Task CreateInstruction([FromBody] CreateInstructionRequest model) + { + var instruction = await _instructionService.CreateInstruction(model); + return Ok(instruction); + } + + /// + /// Update an existing instruction. + /// + /// The instruction model. Id must match the object which is being updated. + /// + /// + /// If the instruction is not found + [HttpPut] + [Authorize(Policy = "Admin")] + public async Task UpdateInstruction([FromBody] UpdateInstructionRequest model) + { + var instruction = await _instructionService.UpdateInstructionById(model); + return Ok(instruction); + } + + /// + /// Delete an existing instruction. + /// + /// The ID of the instruction to delete. + /// + /// + /// If the instruction is not found + [HttpDelete] + [Authorize(Policy = "Admin")] + public async Task DeleteInstruction(long id) + { + try + { + return Ok(await _instructionService.DeleteInstructionById(id)); + } + catch (InstructionNotFoundException) + { + return NotFound(); + } + } + + /// + /// Retrieve all instructions for the authenticated user. + /// + /// A list of for the user. + /// Returns the list of all instructions + [HttpGet("all")] + public async Task GetAllInstructions() + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + return Ok(_instructionService.GetAllInstructions(userId)); + } + + /// + /// Retrieve all completed instructions for the authenticated user. + /// + /// A list of that are completed for the user. + /// Returns the list of completed instructions + [HttpGet("completed")] + public async Task GetCompletedInstructions() + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + return Ok(_instructionService.GetCompletedInstructions(userId)); + } + + /// + /// Retrieve all unfinished instructions for the authenticated user. + /// + /// A list of that are unfinished for the user. + /// Returns the list of unfinished instructions + [HttpGet("unfinished")] + public async Task GetUnfinishedInstructions() + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + return Ok(_instructionService.GetUnfinishedInstructions(userId)); + } + + /// + /// Retrieve instructions by category ID for the authenticated user. + /// + /// The ID of the category to filter instructions. + /// A list of for the specified category. + /// Returns the list of instructions for the specified category + /// If the category is not found + [HttpGet("category/{id}")] + public async Task GetInstructionsByCategoryId(long id) + { + try + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + return Ok(_instructionService.GetInstructionsByCategoryId(userId, id)); + } + catch (CategoryNotFoundException) + { + return NotFound(); + } + } + + /// + /// Retrieve a specific instruction by its ID for the authenticated user. + /// + /// The ID of the instruction to retrieve. + /// for the specified instruction. + /// Returns the instruction with the specified ID + /// If the instruction is not found + [HttpGet("{id}")] + public async Task GetInstructionById(long id) + { + try + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + return Ok(_instructionService.GetInstructionById(userId, id)); + } + catch(InstructionNotFoundException) + { + return NotFound(); + } + } +} + diff --git a/Controllers/InstructionTestController.cs b/Controllers/InstructionTestController.cs new file mode 100644 index 0000000..38123a4 --- /dev/null +++ b/Controllers/InstructionTestController.cs @@ -0,0 +1,278 @@ +using AutoMapper; +using GamificationService.Exceptions.Services.Instruction; +using GamificationService.Exceptions.Services.InstructionTest; +using GamificationService.Models.BasicResponses; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Services.InstructionTests; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace GamificationService.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = "User")] +public class InstructionTestController : ControllerBase +{ + private readonly IInstructionTestsService _instructionTestsService; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public InstructionTestController(IInstructionTestsService instructionTestsService, UserManager userManager, ILogger logger, IMapper mapper) + { + _instructionTestsService = instructionTestsService; + _userManager = userManager; + _logger = logger; + _mapper = mapper; + } + + /// + /// Gets an instruction test by its ID. + /// + /// The ID of the instruction test. + /// An containing the instruction test DTO if found, or a 404 Not Found if not found. + /// Returns the instruction test DTO + /// If the instruction test is not found + [HttpGet("{id}")] + public IActionResult GetInstructionTestById(long id) + { + // TODO: verify admin access / user ownership + try + { + var instructionTest = _instructionTestsService.GetInstructionTestById(id); + return Ok(_mapper.Map(instructionTest)); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets an instruction test by its instruction ID. + /// + /// The ID of the instruction. + /// An containing the instruction test DTO if found, or a 404 Not Found if not found. + /// Returns the instruction test DTO + /// If the instruction is not found + [HttpGet("instruction/{id}")] + public IActionResult GetInstructionTestsByInstructionId(long id) + { + // TODO: verify admin access / user ownership + try + { + var instructionTest = _instructionTestsService.GetInstructionTestsByInstructionId(id); + return Ok(_mapper.Map(instructionTest)); + } + catch (InstructionTestNotFoundException) + { + return Ok(new List()); + } + catch (InstructionNotFoundException) + { + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Instruction not found" + }); + } + } + + /// + /// Gets all instruction test questions by instruction test ID. + /// + /// The ID of the instruction test. + /// A list of containing the instruction test questions if found, or a 404 Not Found if not found. + /// Returns the instruction test questions + /// If the instruction test questions are not found + [HttpGet("{instructionTestId}/questions")] + public IActionResult GetInstructionTestQuestionsByInstructionTestId(long instructionTestId) + { + // TODO: verify admin access / user ownership + try + { + var instructionTestQuestions = _instructionTestsService.GetInstructionTestQuestionsByInstructionTestId(instructionTestId); + return Ok(instructionTestQuestions); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets all instruction test results for authorized user by instruction ID. + /// + /// The ID of the instruction. + /// A list of containing the instruction test results if found, or a 404 Not Found if not found. + /// Returns the instruction test results + /// If the instruction test results are not found + [HttpGet("/{instructionTestId}/results")] + public async Task GetUserInstructionTestResultsByInstructionTestId(long instructionTestId) + { + // TODO: verify user ownership + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + try + { + var instructionTestResults = _instructionTestsService.GetUserInstructionTestResultsByInstructionTestId(userId, instructionTestId); + return Ok(instructionTestResults); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets all instruction test results for a specific user by instruction test ID (admin access). + /// + /// The ID of the user whose results are being requested. + /// The ID of the instruction. + /// A list of containing the instruction test results if found, or a 404 Not Found if not found. + /// Returns the instruction test results + /// If the instruction test results are not found + /// If the user is not an admin + [HttpGet("{instructionTestId}/user/{userId}/results")] + [Authorize(Roles = "Admin")] + public IActionResult GetInstructionTestResultsForUserByInstructionTestId(long userId, long instructionTestId) + { + try + { + var instructionTestResults = _instructionTestsService.GetUserInstructionTestResultsByInstructionTestId(userId, instructionTestId); + return Ok(instructionTestResults); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets all instruction test results for a user by user ID. + /// + /// The ID of the user. + /// A list of containing the instruction test results if found, or a 404 Not Found if not found. + /// Returns the instruction test results + /// If the instruction test results are not found + [HttpGet("user/{id}/results")] + public IActionResult GetInstructionTestResultsByUserId(long id) + { + // TODO: verify admin access / user ownership + try + { + var instructionTestResults = _instructionTestsService.GetInstructionTestResultsByUserId(id); + return Ok(instructionTestResults); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets all completed instruction test results for a user by user ID. + /// + /// The ID of the user. + /// A list of containing the instruction test results if found, or a 404 Not Found if not found. + /// Returns the instruction test results + /// If the instruction test results are not found + [HttpGet("user/{id}/completed")] + public IActionResult GetCompletedInstructionTestsByUserId(long id) + { + // TODO: verify admin access / user ownership + try + { + var instructionTestResults = _instructionTestsService.GetCompletedInstructionTestsByUserId(id); + return Ok(instructionTestResults); + } + catch (InstructionTestNotFoundException) + { + return NotFound(); + } + } + + /// + /// Creates a new instruction test. + /// + /// The instruction test model. + /// A containing the created instruction test if successful, or a 500 Internal Server Error if not successful. + /// Returns the created instruction test + [HttpPost] + public async Task CreateInstructionTest([FromBody] InstructionTestCreateDTO model) + { + try + { + var instructionTest = await _instructionTestsService.CreateInstructionTest(model); + return Ok(instructionTest); + } + catch (Exception) + { + return StatusCode(500, "Failed to create instruction test"); + } + } + + /// + /// Updates an existing instruction test. + /// + /// The instruction test model. + /// A containing the updated instruction test if successful, or a 500 Internal Server Error if not successful. + /// Returns the updated instruction test + [HttpPut] + [Authorize(Policy = "Admin")] + public async Task UpdateInstructionTest([FromBody] InstructionTestCreateDTO model) + { + try + { + var instructionTest = await _instructionTestsService.UpdateInstructionTest(model); + return Ok(instructionTest); + } + catch (Exception) + { + return StatusCode(500, "Failed to update instruction test"); + } + } + + /// + /// Deletes an existing instruction test. + /// + /// The ID of the instruction test to delete. + /// A + /// Returns the deletion status. + [HttpDelete("{id}")] + [Authorize(Policy = "Admin")] + public async Task DeleteInstructionTest(long id) + { + try + { + await _instructionTestsService.DeleteInstructionTestByIdAsync(id); + return Ok(); + } + catch (Exception) + { + return StatusCode(500, "Failed to delete instruction test"); + } + } + + [HttpPost("submit")] + public async Task SubmitInstructionTest([FromBody] InstructionTestSubmissionDTO model) + { + // TODO: verify user access + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + try + { + await _instructionTestsService.SubmitInstructionTestAsync(userId, model); + return Ok(); + } + catch (Exception) + { + return StatusCode(500, "Failed to submit instruction test"); + } + } +} diff --git a/Controllers/RightsController.cs b/Controllers/RightsController.cs new file mode 100644 index 0000000..bbe3f82 --- /dev/null +++ b/Controllers/RightsController.cs @@ -0,0 +1,186 @@ +using GamificationService.Models.BasicResponses; +using GamificationService.Models.DTO; +using GamificationService.Services.Rights; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GamificationService.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = "Admin")] +public class RightsController : ControllerBase +{ + #region Services + + private readonly IRightsService _rightsService; + private readonly ILogger _logger; + + #endregion + + #region Constructor + + public RightsController(IRightsService rightsService, ILogger logger) + { + _rightsService = rightsService; + _logger = logger; + } + + #endregion + + #region Methods + + [HttpGet] + public async Task GetAllRightsAsync([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var (rights, totalCount) = await _rightsService.GetAllRightsAsync(pageNumber, pageSize); + + _logger.LogInformation($"Retrieved {rights.Count} rights"); + + var response = new GetAllRightsResponse() + { + Rights = rights, + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize + }; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to get rights.", + }); + } + } + [HttpGet("{id}")] + public async Task GetRightByIdAsync(long id) + { + var right = await _rightsService.GetRightByIdAsync(id); + _logger.LogInformation($"Retrieved right with id: {id}"); + if (right == null) + { + return NotFound(new BasicResponse() + { + + Code = 404, + Message = "Right not found" + + }); + } + return Ok(right); + } + [HttpPost] + public async Task CreateRightAsync([FromBody] RightDTO model) + { + try + { + var right = await _rightsService.CreateRightAsync(model.Name, model.Description); + + _logger.LogInformation($"Created right: {right}"); + + return CreatedAtAction(nameof(CreateRightAsync), new { id = right.Id }, right); + + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to create right.", + }); + } + } + [HttpPut("{id}")] + public async Task UpdateRightAsync(long id, [FromBody] RightDTO model) + { + try + { + if (await _rightsService.UpdateRightAsync(id, model.Name, model.Description)) + { + _logger.LogInformation($"Updated right: {id}"); + return Ok(new BasicResponse() + { + Code = 200, + Message = "Rights updated", + }); + } + _logger.LogError($"Unknown with right updating, {id}"); + return StatusCode(418, + new BasicResponse() + { + Code = 418, + Message = "Failed to update right." + }); + } + catch (KeyNotFoundException) + { + _logger.LogError($"Right not found, {id} "); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Right not found" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to update right" + }); + } + } + + [HttpDelete("{id}")] + public async Task DeleteRightAsync(long id) + { + try + { + if( await _rightsService.DeleteRightAsync(id)) + { + _logger.LogInformation($"Deleted right: {id}"); + return Ok(new BasicResponse() + { + Code = 200, + Message = "Rights deleted", + }); + } + _logger.LogError($"Unknown error with right deleting, {id} "); + return StatusCode(418, new BasicResponse() + { + Code = 418, + Message = "Failed to delete right" + }); + + } + catch (KeyNotFoundException) + { + _logger.LogError($"Role not found, {id} "); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Right not found" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to delete right" + }); + } + } + + + #endregion +} diff --git a/Controllers/RoleController.cs b/Controllers/RoleController.cs new file mode 100644 index 0000000..f7fe6f9 --- /dev/null +++ b/Controllers/RoleController.cs @@ -0,0 +1,283 @@ +using GamificationService.Models.BasicResponses; +using GamificationService.Models.DTO; +using GamificationService.Services.Roles; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace GamificationService.Controllers; +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = "Admin")] +public class RoleController : ControllerBase +{ + #region Services + + private readonly IRolesService _rolesService; + private readonly ILogger _logger; + + #endregion + + #region Constructor + + public RoleController(ILogger logger, IRolesService rolesService) + { + _logger = logger; + _rolesService = rolesService; + } + + #endregion + + #region ControllerMethods + + [HttpGet] + public async Task GetAllRolesAsync([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var (roles, totalCount) = await _rolesService.GetAllRolesAsync(pageNumber, pageSize); + _logger.LogInformation($"Roles found successfully, {roles.Count}"); + var response = new GetAllRolesResponse() + { + Roles = roles, + TotalCount = totalCount, + PageNumber = pageNumber, + PageSize = pageSize + }; + return Ok(response); + } + catch (Exception ex) + { + _logger.LogError(ex,ex.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to get roles" + }); + } + + } + + + [HttpGet("{id}")] + public async Task GetRoleByIdAsync(long id) + { + var role = await _rolesService.GetRoleByIdAsync(id); + _logger.LogInformation($"Role found successfully, {role.Id}"); + if (role == null) + { + return NotFound(new BasicResponse() + { + + Code = 404, + Message = "Role not found" + + }); + } + return Ok(role); + } + + [HttpPost] + public async Task CreateRoleAsync([FromBody] RoleDTO model) + { + try + { + + var role = await _rolesService.CreateRoleAsync(model.Name, model.Description); + _logger.LogInformation($"Role created successfully, {role.Id}"); + return CreatedAtAction(nameof(CreateRoleAsync), new { id = role.Id }, role); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to create role" + }); + } + } + + + [HttpPut("{id}")] + public async Task UpdateRoleAsync(long id, [FromBody] RoleDTO model) + { + try + { + if (await _rolesService.UpdateRoleAsync(id, model.Name, model.Description)) + { + _logger.LogInformation($"Role updated successfully, {id}"); + + return Ok(new BasicResponse() + { + Code = 200, + Message = "Role updated successfully" + }); + } + + _logger.LogCritical($"Unknown error with role updating, {id}"); + return StatusCode(418,new BasicResponse() + { + Code = 418, + Message = "Role not found" + }); + + } + catch (KeyNotFoundException) + { + _logger.LogError($"Role not found, {id} "); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Role not found" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to update role" + }); + } + + } + + [HttpDelete("{id}")] + public async Task DeleteRoleAsync(long id) + { + try + { + if (await _rolesService.DeleteRoleAsync(id)) + { + + _logger.LogInformation($"Role updated successfully, {id}"); + + return Ok(new BasicResponse() + { + Code = 200, + Message = "Role updated successfully" + }); + } + + _logger.LogCritical($"Unknown error with role deleting, RoleId {id}"); + return StatusCode(418,new BasicResponse() + { + Code = 418, + Message = "Role not found" + }); + } + catch (KeyNotFoundException) + { + _logger.LogError($"Role not found, {id} "); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Role not found" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to delete role" + }); + } + + } + + + [HttpPost("{roleId}/rights/{rightId}")] + public async Task AddRightToRoleAsync(long roleId, long rightId) + { + try + { + if (await _rolesService.AddRightToRoleAsync(roleId, rightId)) + { + _logger.LogInformation($"Right added to role successfully, RoleId: {roleId}, RightId: {rightId}"); + return Ok(new BasicResponse() + { + Code = 200, + Message = "Right added to role successfully" + }); + } + + _logger.LogCritical($"Unknown error with adding right to role, RoleId: {roleId}, RightId: {rightId}"); + return StatusCode(418,new BasicResponse() + { + Code = 418, + Message = "Right not found for role" + }); + } + catch(KeyNotFoundException e) + { + _logger.LogError(e, e.Message); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Right not found for role" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to add right to role" + }); + } + } + + [HttpDelete("{roleId}/rights/{rightId}")] + public async Task RemoveRightFromRoleAsync(long roleId, long rightId) + { + try + { + + if (await _rolesService.RemoveRightFromRoleAsync(roleId, rightId)) + { + _logger.LogInformation($"Right removed from role successfully, RoleId: {roleId}, RightId: {rightId}"); + + return Ok(new BasicResponse() + { + Code = 200, + Message = "Right removed from role successfully" + }); + } + + _logger.LogCritical($"Unknown error with removing right from role, RoleId: {roleId}, RightId: {rightId}"); + return StatusCode(418, new BasicResponse() + { + Code = 418, + Message = "Right not found right for role" + }); + + } + catch (KeyNotFoundException e) + { + _logger.LogError(e, e.Message); + return NotFound(new BasicResponse() + { + Code = 404, + Message = "Right not found for role" + }); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, new BasicResponse() + { + Code = 500, + Message = "Failed to remove right from role" + }); + } + } + + + + #endregion +} diff --git a/Controllers/UserProfileController.cs b/Controllers/UserProfileController.cs new file mode 100644 index 0000000..030b08e --- /dev/null +++ b/Controllers/UserProfileController.cs @@ -0,0 +1,184 @@ +using AutoMapper; +using GamificationService.Exceptions.Services.ProfileService; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Services.UsersProfile; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace GamificationService.Controllers; + +[ApiController] +[Authorize(Policy = "User")] +[Route("api/[controller]")] +public class UserProfileController : ControllerBase +{ + private readonly IUserProfileService _userProfilesService; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public UserProfileController(IUserProfileService userProfilesService, UserManager userManager, ILogger logger, IMapper mapper) + { + _userProfilesService = userProfilesService; + _userManager = userManager; + _logger = logger; + _mapper = mapper; + } + + /// + /// Gets a user profile by its ID. + /// + /// The username of the user profile's owner. + /// An containing the user profile DTO if found, or a 404 Not Found if not found. + /// Returns the user profile DTO + /// If the user profile is not found + [HttpGet("user/{username}")] + public async Task GetUserProfileByUsername(string username) + { + try + { + var user = (await _userManager.FindByNameAsync(username)); + if (user == null) + { + return NotFound(); + } + + var userProfile = _userProfilesService.GetUserProfileByUserId(user.Id); + return Ok(_mapper.Map(userProfile)); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + + /// + /// Gets a user profile by its ID. + /// + /// The ID of the user profile. + /// An containing the user profile DTO if found, or a 404 Not Found if not found. + /// Returns the user profile DTO + /// If the user profile is not found + [HttpGet("{id}")] + public IActionResult GetUserProfileById(long id) + { + try + { + var userProfile = _userProfilesService.GetUserProfileById(id); + return Ok(_mapper.Map(userProfile)); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + + /// + /// Adds a new user profile. + /// + /// The username of the user. + /// The user profile model. + /// A containing the created user profile if successful, or a 500 Internal Server Error if not successful. + /// Returns the created user profile + /// If the user is not found + [HttpPost("user/{username}")] + [Authorize(Policy = "Admin")] + public async Task AddUserProfile(string username, [FromBody] UserProfileCreateDTO model) + { + var user = (await _userManager.FindByNameAsync(username)); + if (user == null) + { + return NotFound(); + } + + try + { + var userProfile = await _userProfilesService.AddUserProfile(user.Id, model); + return Ok(_mapper.Map(userProfile)); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + + /// + /// Update user profile for the logged in user. + /// + /// The user profile model. + /// A containing the updated user profile if successful, or a 500 Internal Server Error if not successful. + /// Returns the updated user profile + /// If the user profile is not found + [HttpPut] + public async Task UpdateUserProfile([FromBody] UserProfileCreateDTO model) + { + string username = User.Claims.First(c => c.Type == "username").Value; + long userId = (await _userManager.FindByNameAsync(username))!.Id; + + try + { + bool result = await _userProfilesService.UpdateUserProfileByUserId(userId, model); + return Ok(result); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + + /// + /// Updates an existing user profile. + /// + /// The username of the user. + /// The user profile model. + /// A containing the updated user profile if successful, or a 500 Internal Server Error if not successful. + /// Returns the updated user profile + /// If the user profile is not found + [HttpPut] + [Authorize(Policy = "Admin")] + [Route("user/{userId}")] + public async Task UpdateUserProfileByUsername(string username, [FromBody] UserProfileCreateDTO model) + { + var user = (await _userManager.FindByNameAsync(username)); + if (user == null) + { + return NotFound(); + } + + try + { + bool result = await _userProfilesService.UpdateUserProfileByUserId(user.Id, model); + return Ok(result); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + + /// + /// Deletes an existing user profile. + /// + /// The ID of the user profile to delete. + /// A + /// Returns true. + /// If the user profile is not found + [HttpDelete("{id}")] + [Authorize(Policy = "Admin")] + public IActionResult DeleteUserProfile(long id) + { + try + { + _userProfilesService.DeleteUserProfile(id); + return Ok(); + } + catch (ProfileNotFoundException) + { + return NotFound(); + } + } + +} + diff --git a/Database/ApplicationContext.cs b/Database/ApplicationContext.cs new file mode 100755 index 0000000..f87e48b --- /dev/null +++ b/Database/ApplicationContext.cs @@ -0,0 +1,50 @@ +using GamificationService.Models.Database; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace GamificationService.Database; + + +public class ApplicationContext : IdentityDbContext +{ + public ApplicationContext(DbContextOptions options) : base(options) + { + } + + public DbSet Rights { get; set; } + public DbSet RefreshTokens { get; set; } + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + public DbSet UserRoles { get; set; } + public DbSet RoleRights { get; set; } + public DbSet UserProfiles { get; set; } + public DbSet Instructions { get; set; } + public DbSet InstructionCategories { get; set; } + public DbSet InstructionParagraphs { get; set; } + public DbSet InstructionTests { get; set; } + public DbSet InstructionTestQuestions { get; set; } + public DbSet InstructionTestResults { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(ur => new { ur.UserId, ur.RoleId }); + + modelBuilder.Entity() + .HasKey(rr => new { rr.RoleId, rr.RightId }); + + modelBuilder.Entity() + .HasMany(itq => itq.Questions); + + modelBuilder.Entity() + .HasOne(itr => itr.InstructionTest); + + modelBuilder.Entity() + .HasOne(i => i.Category); + + modelBuilder.Entity() + .HasMany(i => i.Paragraphs); + } +} diff --git a/Database/Extensions/ChangeTrackerExtensions.cs b/Database/Extensions/ChangeTrackerExtensions.cs new file mode 100755 index 0000000..7927f16 --- /dev/null +++ b/Database/Extensions/ChangeTrackerExtensions.cs @@ -0,0 +1,54 @@ +using GamificationService.Models.Database; +using GamificationService.Services.CurrentUsers; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace GamificationService.Database.Extensions; + +public static class ChangeTrackerExtensions +{ + public static void SetAuditProperties(this ChangeTracker changeTracker, ICurrentUserService currentUserService) + { + changeTracker.DetectChanges(); + IEnumerable entities = + changeTracker + .Entries() + .Where(t => t.Entity is AuditableEntity && + ( + t.State == EntityState.Deleted + || t.State == EntityState.Added + || t.State == EntityState.Modified + )); + + if (entities.Any()) + { + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + + string user = currentUserService.GetCurrentUser().Login ?? "Unknown"; + + foreach (EntityEntry entry in entities) + { + AuditableEntity entity = (AuditableEntity)entry.Entity; + + switch (entry.State) + { + case EntityState.Added: + entity.CreatedOn = timestamp; + entity.CreatedBy = user; + entity.UpdatedOn = timestamp; + entity.UpdatedBy = user; + break; + case EntityState.Modified: + entity.UpdatedOn = timestamp; + entity.UpdatedBy = user; + break; + case EntityState.Deleted: + entity.UpdatedOn = timestamp; + entity.UpdatedBy = user; + entry.State = EntityState.Deleted; + break; + } + } + } + } +} diff --git a/Database/Repositories/GenericRepository.cs b/Database/Repositories/GenericRepository.cs new file mode 100755 index 0000000..30a8c94 --- /dev/null +++ b/Database/Repositories/GenericRepository.cs @@ -0,0 +1,98 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace GamificationService.Database.Repositories; + +public class GenericRepository where TEntity : class +{ + internal ApplicationContext context; + internal DbSet dbSet; + + public GenericRepository(ApplicationContext context) + { + this.context = context; + this.dbSet = context.Set(); + } + + public virtual IQueryable Get( + Expression> filter = null, + Func, IOrderedQueryable> orderBy = null, + string includeProperties = "") + { + IQueryable query = dbSet; + + if (filter != null) + { + query = query.Where(filter); + } + + foreach (var includeProperty in includeProperties.Split + (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + query = query.Include(includeProperty); + } + + if (orderBy != null) + { + return orderBy(query); + } + else + { + return query; + } + } + + public virtual TEntity? GetByID(object id) + { + return dbSet.Find(id); + } + + public async virtual Task GetByIDAsync(object id) + { + return await dbSet.FindAsync(id); + } + public virtual void Insert(TEntity entity) + { + dbSet.Add(entity); + } + public virtual void InsertRange(IEnumerable entities) + { + dbSet.AddRange(entities); + } + public async virtual Task InsertAsync(TEntity entity) + { + await dbSet.AddAsync(entity); + } + public virtual void Delete(object id) + { + TEntity? entityToDelete = dbSet.Find(id); + + if (entityToDelete == null) + { + // It's probably a good idea to throw an error here + // but I'm leaving it as is for now + return; + } + + Delete(entityToDelete); + } + public virtual void DeleteRange(IEnumerable entities) + { + dbSet.RemoveRange(entities); + } + public virtual void Delete(TEntity entityToDelete) + { + if (context.Entry(entityToDelete).State == EntityState.Detached) + { + dbSet.Attach(entityToDelete); + } + dbSet.Remove(entityToDelete); + } + + public virtual void Update(TEntity entityToUpdate) + { + dbSet.Attach(entityToUpdate); + context.Entry(entityToUpdate).State = EntityState.Modified; + } +} diff --git a/Database/Repositories/UnitOfWork.cs b/Database/Repositories/UnitOfWork.cs new file mode 100755 index 0000000..eeee6a4 --- /dev/null +++ b/Database/Repositories/UnitOfWork.cs @@ -0,0 +1,239 @@ +using GamificationService.Models.Database; +using Microsoft.EntityFrameworkCore.Storage; + +namespace GamificationService.Database.Repositories; + +public class UnitOfWork : IDisposable +{ + #region fields + + private ApplicationContext _context; + private GenericRepository _userProfileRepository; + private GenericRepository _roleRepository; + private GenericRepository _rightRepository; + private GenericRepository _refreshTokenRepository; + private GenericRepository _roleRightRepository; + private GenericRepository _userRoleRepository; + private GenericRepository _instructionRepository; + private GenericRepository _instructionParagraphRepository; + private GenericRepository _instructionCategoryRepository; + private GenericRepository _instructionTestRepository; + private GenericRepository _instructionTestQuestionRepository; + private GenericRepository _instructionTestResultRepository; + + #endregion + + + private IDbContextTransaction _transaction; + + public UnitOfWork(ApplicationContext context) + { + _context = context; + } + + + #region Properties + + public GenericRepository UserProfileRepository + { + get + { + if (this._userProfileRepository == null) + { + this._userProfileRepository = new GenericRepository(_context); + } + return _userProfileRepository; + } + } + + public GenericRepository RoleRepository + { + get + { + if (this._roleRepository == null) + { + this._roleRepository = new GenericRepository(_context); + } + return _roleRepository; + } + } + + public GenericRepository RightRepository + { + get + { + if (this._rightRepository == null) + { + this._rightRepository = new GenericRepository(_context); + } + return _rightRepository; + } + } + + public GenericRepository RefreshTokenRepository + { + get + { + if (this._refreshTokenRepository == null) + { + this._refreshTokenRepository = new GenericRepository(_context); + } + return _refreshTokenRepository; + } + } + + public GenericRepository RoleRightRepository + { + get + { + if (this._roleRightRepository == null) + { + this._roleRightRepository = new GenericRepository(_context); + } + return _roleRightRepository; + } + } + + public GenericRepository UserRoleRepository + { + get + { + if (this._userRoleRepository == null) + { + this._userRoleRepository = new GenericRepository(_context); + } + return _userRoleRepository; + } + } + + public GenericRepository InstructionRepository + { + get + { + if (this._instructionRepository == null) + { + this._instructionRepository = new GenericRepository(_context); + } + return _instructionRepository; + } + } + + public GenericRepository InstructionParagraphRepository + { + get + { + if (this._instructionParagraphRepository == null) + { + this._instructionParagraphRepository = new GenericRepository(_context); + } + return _instructionParagraphRepository; + } + } + + public GenericRepository InstructionCategoryRepository + { + get + { + if (this._instructionCategoryRepository == null) + { + this._instructionCategoryRepository = new GenericRepository(_context); + } + return _instructionCategoryRepository; + } + } + + public GenericRepository InstructionTestRepository + { + get + { + if (this._instructionTestRepository == null) + { + this._instructionTestRepository = new GenericRepository(_context); + } + return _instructionTestRepository; + } + } + + public GenericRepository InstructionTestQuestionRepository + { + get + { + if (this._instructionTestQuestionRepository == null) + { + this._instructionTestQuestionRepository = new GenericRepository(_context); + } + return _instructionTestQuestionRepository; + } + } + + public GenericRepository InstructionTestResultRepository + { + get + { + if (this._instructionTestResultRepository == null) + { + this._instructionTestResultRepository = new GenericRepository(_context); + } + return _instructionTestResultRepository; + } + } + + #endregion + + + + + public bool Save() + { + return _context.SaveChanges() > 0; + } + + public async Task SaveAsync() + { + return await _context.SaveChangesAsync() > 0; + } + + + private bool disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + _context.Dispose(); + } + } + this.disposed = true; + } + public async Task BeginTransactionAsync() + { + if (_transaction is not null) + throw new InvalidOperationException("A transaction has already been started."); + _transaction = await _context.Database.BeginTransactionAsync(); + } + public async Task CommitAsync() + { + if (_transaction is null) + throw new InvalidOperationException("A transaction has not been started."); + + try + { + await _transaction.CommitAsync(); + _transaction.Dispose(); + _transaction = null; + } + catch (Exception) + { + if (_transaction is not null) + await _transaction.RollbackAsync(); + throw; + } + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..2c06cee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["PurpleHackBackend/PurpleHackBackend.csproj", "PurpleHackBackend/"] +RUN dotnet restore "PurpleHackBackend/PurpleHackBackend.csproj" +COPY . . +WORKDIR "/src/PurpleHackBackend" +RUN dotnet build "PurpleHackBackend.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "PurpleHackBackend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PurpleHackBackend.dll"] diff --git a/Exceptions/Services/AuthService/AuthServiceException.cs b/Exceptions/Services/AuthService/AuthServiceException.cs new file mode 100755 index 0000000..6f27e4f --- /dev/null +++ b/Exceptions/Services/AuthService/AuthServiceException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.AuthService; + +/// +/// Represents an exception related to authentication service operations. +/// +public class AuthServiceException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public AuthServiceException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public AuthServiceException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public AuthServiceException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/Services/Instruction/CategoryNotFoundException.cs b/Exceptions/Services/Instruction/CategoryNotFoundException.cs new file mode 100644 index 0000000..343e5f4 --- /dev/null +++ b/Exceptions/Services/Instruction/CategoryNotFoundException.cs @@ -0,0 +1,25 @@ + +namespace GamificationService.Exceptions.Services.Instruction; + +public class CategoryNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public CategoryNotFoundException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public CategoryNotFoundException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public CategoryNotFoundException(string message, Exception innerException) : base(message, innerException) { } +} + + diff --git a/Exceptions/Services/Instruction/InstructionAccessException.cs b/Exceptions/Services/Instruction/InstructionAccessException.cs new file mode 100644 index 0000000..52ba9c5 --- /dev/null +++ b/Exceptions/Services/Instruction/InstructionAccessException.cs @@ -0,0 +1,24 @@ +namespace GamificationService.Exceptions.Services.Instruction; + +public class InstructionAccessException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionAccessException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionAccessException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionAccessException(string message, Exception innerException) : base(message, innerException) { } +} + + diff --git a/Exceptions/Services/Instruction/InstructionCreationException.cs b/Exceptions/Services/Instruction/InstructionCreationException.cs new file mode 100644 index 0000000..a577386 --- /dev/null +++ b/Exceptions/Services/Instruction/InstructionCreationException.cs @@ -0,0 +1,24 @@ +namespace GamificationService.Exceptions.Services.Instruction; + +public class InstructionCreationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionCreationException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionCreationException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionCreationException(string message, Exception innerException) : base(message, innerException) { } +} + + diff --git a/Exceptions/Services/Instruction/InstructionDeletionException.cs b/Exceptions/Services/Instruction/InstructionDeletionException.cs new file mode 100644 index 0000000..ee54a05 --- /dev/null +++ b/Exceptions/Services/Instruction/InstructionDeletionException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.Instruction; + +public class InstructionDeletionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionDeletionException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionDeletionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionDeletionException(string message, Exception innerException) : base(message, innerException) { } +} + + + diff --git a/Exceptions/Services/Instruction/InstructionNotFoundException.cs b/Exceptions/Services/Instruction/InstructionNotFoundException.cs new file mode 100644 index 0000000..d035514 --- /dev/null +++ b/Exceptions/Services/Instruction/InstructionNotFoundException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.Instruction; + +public class InstructionNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionNotFoundException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionNotFoundException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionNotFoundException(string message, Exception innerException) : base(message, innerException) { } +} + + + diff --git a/Exceptions/Services/Instruction/InstructionUpdateException.cs b/Exceptions/Services/Instruction/InstructionUpdateException.cs new file mode 100644 index 0000000..f68552e --- /dev/null +++ b/Exceptions/Services/Instruction/InstructionUpdateException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.Instruction; + +public class InstructionUpdateException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionUpdateException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionUpdateException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionUpdateException(string message, Exception innerException) : base(message, innerException) { } +} + + + diff --git a/Exceptions/Services/InstructionTest/InstructionTestConflictException.cs b/Exceptions/Services/InstructionTest/InstructionTestConflictException.cs new file mode 100644 index 0000000..711451d --- /dev/null +++ b/Exceptions/Services/InstructionTest/InstructionTestConflictException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.InstructionTest; + +/// +/// Represents an exception that occurs when there is a conflict in instruction test operations. +/// +public class InstructionTestConflictException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionTestConflictException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionTestConflictException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionTestConflictException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/Services/InstructionTest/InstructionTestCreationException.cs b/Exceptions/Services/InstructionTest/InstructionTestCreationException.cs new file mode 100644 index 0000000..8d78560 --- /dev/null +++ b/Exceptions/Services/InstructionTest/InstructionTestCreationException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.InstructionTest; + +/// +/// Represents an exception that occurs during the creation of an instruction test. +/// +public class InstructionTestCreationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionTestCreationException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionTestCreationException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionTestCreationException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/Services/InstructionTest/InstructionTestDeletionException.cs b/Exceptions/Services/InstructionTest/InstructionTestDeletionException.cs new file mode 100644 index 0000000..bb7f6bf --- /dev/null +++ b/Exceptions/Services/InstructionTest/InstructionTestDeletionException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.InstructionTest; + +/// +/// Represents an exception that occurs during the deletion of an instruction test. +/// +public class InstructionTestDeletionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionTestDeletionException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InstructionTestDeletionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionTestDeletionException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/Services/InstructionTest/InstructionTestNotFoundException.cs b/Exceptions/Services/InstructionTest/InstructionTestNotFoundException.cs new file mode 100644 index 0000000..b436799 --- /dev/null +++ b/Exceptions/Services/InstructionTest/InstructionTestNotFoundException.cs @@ -0,0 +1,27 @@ +namespace GamificationService.Exceptions.Services.InstructionTest; + +/// +/// Represents an exception that occurs when an instruction test is not found. +/// +public class InstructionTestNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionTestNotFoundException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + + /// The message that describes the error. + public InstructionTestNotFoundException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionTestNotFoundException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/Services/InstructionTest/InstructionTestSubmissionException.cs b/Exceptions/Services/InstructionTest/InstructionTestSubmissionException.cs new file mode 100644 index 0000000..fb2c90d --- /dev/null +++ b/Exceptions/Services/InstructionTest/InstructionTestSubmissionException.cs @@ -0,0 +1,27 @@ +namespace GamificationService.Exceptions.Services.InstructionTest; + +/// +/// Represents an exception that occurs when an instruction test submission is failed. +/// +public class InstructionTestSubmissionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InstructionTestSubmissionException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + + /// The message that describes the error. + public InstructionTestSubmissionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InstructionTestSubmissionException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/Services/InstructionTest/InstructionTestUpdateException.cs b/Exceptions/Services/InstructionTest/InstructionTestUpdateException.cs new file mode 100644 index 0000000..e69de29 diff --git a/Exceptions/Services/JwtService/GenerateRefreshTokenException.cs b/Exceptions/Services/JwtService/GenerateRefreshTokenException.cs new file mode 100755 index 0000000..9b982e2 --- /dev/null +++ b/Exceptions/Services/JwtService/GenerateRefreshTokenException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.JwtService; + +/// +/// Represents an exception related to jwt token service operations. +/// +public class GenerateRefreshTokenException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public GenerateRefreshTokenException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public GenerateRefreshTokenException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public GenerateRefreshTokenException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/Services/JwtService/JwtServiceException.cs b/Exceptions/Services/JwtService/JwtServiceException.cs new file mode 100755 index 0000000..3d5d896 --- /dev/null +++ b/Exceptions/Services/JwtService/JwtServiceException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.JwtService; + +/// +/// Represents an exception related to jwt token service operations. +/// +public class JwtServiceException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public JwtServiceException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public JwtServiceException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public JwtServiceException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/Services/User/UserNotFoundException.cs b/Exceptions/Services/User/UserNotFoundException.cs new file mode 100755 index 0000000..0a77adc --- /dev/null +++ b/Exceptions/Services/User/UserNotFoundException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.User; + +/// +/// Represents an exception that occurs when a user is not found. +/// +public class UserNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public UserNotFoundException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UserNotFoundException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public UserNotFoundException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/Services/User/UserServiceException.cs b/Exceptions/Services/User/UserServiceException.cs new file mode 100755 index 0000000..3824846 --- /dev/null +++ b/Exceptions/Services/User/UserServiceException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.Services.User; + +/// +/// Represents an exception related to user service operations. +/// +public class UserServiceException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public UserServiceException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public UserServiceException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public UserServiceException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/UserProfileService/ProfileCreationException.cs b/Exceptions/UserProfileService/ProfileCreationException.cs new file mode 100644 index 0000000..2bf5a45 --- /dev/null +++ b/Exceptions/UserProfileService/ProfileCreationException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.ProfileService; + +/// +/// Represents an exception that occurs during profile creation operations. +/// +public class ProfileCreationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ProfileCreationException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ProfileCreationException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProfileCreationException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/UserProfileService/ProfileDeletionException.cs b/Exceptions/UserProfileService/ProfileDeletionException.cs new file mode 100644 index 0000000..73f4713 --- /dev/null +++ b/Exceptions/UserProfileService/ProfileDeletionException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.ProfileService; + +/// +/// Represents an exception that occurs during profile deletion operations. +/// +public class ProfileDeletionException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ProfileDeletionException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ProfileDeletionException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProfileDeletionException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/UserProfileService/ProfileExistsException.cs b/Exceptions/UserProfileService/ProfileExistsException.cs new file mode 100644 index 0000000..a16b7d6 --- /dev/null +++ b/Exceptions/UserProfileService/ProfileExistsException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.ProfileService; + +/// +/// Represents an exception that occurs when a profile already exists. +/// +public class ProfileExistsException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ProfileExistsException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ProfileExistsException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProfileExistsException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/UserProfileService/ProfileNotFoundException.cs b/Exceptions/UserProfileService/ProfileNotFoundException.cs new file mode 100644 index 0000000..eab50d9 --- /dev/null +++ b/Exceptions/UserProfileService/ProfileNotFoundException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.ProfileService; + +/// +/// Represents an exception that occurs when a profile is not found. +/// +public class ProfileNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ProfileNotFoundException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ProfileNotFoundException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProfileNotFoundException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/UserProfileService/ProfileUpdateException.cs b/Exceptions/UserProfileService/ProfileUpdateException.cs new file mode 100644 index 0000000..6ae27a3 --- /dev/null +++ b/Exceptions/UserProfileService/ProfileUpdateException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.Services.ProfileService; + +/// +/// Represents an exception that occurs during profile update operations. +/// +public class ProfileUpdateException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ProfileUpdateException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ProfileUpdateException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ProfileUpdateException(string message, Exception innerException) : base(message, innerException) { } +} + diff --git a/Exceptions/UtilServices/Api/ApiException.cs b/Exceptions/UtilServices/Api/ApiException.cs new file mode 100755 index 0000000..1b653e2 --- /dev/null +++ b/Exceptions/UtilServices/Api/ApiException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.UtilServices.Api; + +/// +/// Represents an exception related to api operations. +/// +public class ApiException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ApiException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ApiException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ApiException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Api/BadRequestException.cs b/Exceptions/UtilServices/Api/BadRequestException.cs new file mode 100755 index 0000000..6c2e2f4 --- /dev/null +++ b/Exceptions/UtilServices/Api/BadRequestException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.UtilServices.Api; +/// +/// Represents an exception related to api operations. +/// +public class BadRequestException : ApiException +{ + /// + /// Initializes a new instance of the class. + /// + public BadRequestException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BadRequestException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public BadRequestException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Api/ForbiddenException.cs b/Exceptions/UtilServices/Api/ForbiddenException.cs new file mode 100755 index 0000000..041482f --- /dev/null +++ b/Exceptions/UtilServices/Api/ForbiddenException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.UtilServices.Api; + +/// +/// Represents an exception related to api operations. +/// +public class ForbiddenException : ApiException +{ + /// + /// Initializes a new instance of the class. + /// + public ForbiddenException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public ForbiddenException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ForbiddenException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Cookies/CookiesException.cs b/Exceptions/UtilServices/Cookies/CookiesException.cs new file mode 100755 index 0000000..84bce9f --- /dev/null +++ b/Exceptions/UtilServices/Cookies/CookiesException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.UtilServices.Cookies; + +/// +/// Represents an exception related to cookie operations. +/// +public class CookiesException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public CookiesException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public CookiesException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public CookiesException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Cookies/DeleteCookiesException.cs b/Exceptions/UtilServices/Cookies/DeleteCookiesException.cs new file mode 100755 index 0000000..f75ee53 --- /dev/null +++ b/Exceptions/UtilServices/Cookies/DeleteCookiesException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.UtilServices.Cookies; + +/// +/// Represents an exception related to deleting cookies. +/// +public class DeleteCookiesException : CookiesException +{ + /// + /// Initializes a new instance of the class. + /// + public DeleteCookiesException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public DeleteCookiesException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public DeleteCookiesException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Cookies/SetCookiesException.cs b/Exceptions/UtilServices/Cookies/SetCookiesException.cs new file mode 100755 index 0000000..6f8f460 --- /dev/null +++ b/Exceptions/UtilServices/Cookies/SetCookiesException.cs @@ -0,0 +1,26 @@ +namespace GamificationService.Exceptions.UtilServices.Cookies; + +/// +/// Represents an exception related to setting cookies. +/// +public class SetCookiesException : CookiesException +{ + /// + /// Initializes a new instance of the class. + /// + public SetCookiesException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public SetCookiesException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public SetCookiesException(string message, Exception innerException) : base(message, innerException) { } + +} diff --git a/Exceptions/UtilServices/Email/EmailException.cs b/Exceptions/UtilServices/Email/EmailException.cs new file mode 100755 index 0000000..8e252a5 --- /dev/null +++ b/Exceptions/UtilServices/Email/EmailException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.UtilServices.Email; + +/// +/// Represents an exception related to email operations. +/// +public class EmailException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public EmailException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public EmailException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public EmailException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/UtilServices/Email/SendEmailException.cs b/Exceptions/UtilServices/Email/SendEmailException.cs new file mode 100755 index 0000000..bc7a97a --- /dev/null +++ b/Exceptions/UtilServices/Email/SendEmailException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.UtilServices.Email; + +/// +/// Represents an exception that occurs during the process of sending an email. +/// +public class SendEmailException : EmailException +{ + /// + /// Initializes a new instance of the class. + /// + public SendEmailException() : base("An error occurred while sending the email.") { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public SendEmailException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public SendEmailException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/UtilServices/JWT/GenerateJWTTokenException.cs b/Exceptions/UtilServices/JWT/GenerateJWTTokenException.cs new file mode 100755 index 0000000..3d0e380 --- /dev/null +++ b/Exceptions/UtilServices/JWT/GenerateJWTTokenException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.UtilServices.JWT; + +/// +/// Represents an exception that occurs while generating a JWT token. +/// +public class GenerateJWTTokenException : JWTException +{ + /// + /// Initializes a new instance of the class. + /// + public GenerateJWTTokenException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public GenerateJWTTokenException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public GenerateJWTTokenException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Exceptions/UtilServices/JWT/JWTException.cs b/Exceptions/UtilServices/JWT/JWTException.cs new file mode 100755 index 0000000..671f60b --- /dev/null +++ b/Exceptions/UtilServices/JWT/JWTException.cs @@ -0,0 +1,25 @@ +namespace GamificationService.Exceptions.UtilServices.JWT; + +/// +/// Represents an exception related to JWT (JSON Web Token) operations. +/// +public class JWTException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public JWTException() : base() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public JWTException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public JWTException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/Extensions/DependencyInjectionExtensions.cs b/Extensions/DependencyInjectionExtensions.cs new file mode 100755 index 0000000..11532aa --- /dev/null +++ b/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,204 @@ +using System.Net; +using System.Net.Mail; +using System.Reflection; +using System.Text; +using GamificationService.Database; +using GamificationService.Database.Repositories; +using GamificationService.Logs; +using GamificationService.Mapper; +using GamificationService.Services.Cookies; +using GamificationService.Services.CurrentUsers; +using GamificationService.Services.InstructionTests; +using GamificationService.Services.JWT; +using GamificationService.Services.NotificationService; +using GamificationService.Services.Rights; +using GamificationService.Services.Roles; +using GamificationService.Services.UsersProfile; +using GamificationService.Utils; +using GamificationService.Utils.Factory; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Serilog; + +namespace GamificationService.Extensions; + +public static class MappingExtensions +{ + public static IServiceCollection AddMapping(this IServiceCollection services) + { + services.AddAutoMapper(typeof(MappingProfile)); + return services; + } +} + +public static class CachingExtensions +{ + public static IServiceCollection AddRedisCaching(this IServiceCollection services, IConfiguration configuration) + { + services.AddStackExchangeRedisCache(options => + { + options.Configuration = configuration["Redis:ConnectionString"] ?? "localhost:6379"; + options.InstanceName = configuration["Redis:InstanceName"] ?? "default"; + }); + return services; + } +} + +public static class LoggingExtensions +{ + public static IHostBuilder UseCustomSerilog(this IHostBuilder hostBuilder) + { + LoggingConfigurator.ConfigureLogging(); + return hostBuilder.UseSerilog(); + } +} + +public static class DatabaseExtensions +{ + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(x => + { + var dbSettings = configuration.GetSection("DatabaseSettings"); + var hostname = dbSettings["Hostname"] ?? "localhost"; + var port = dbSettings["Port"] ?? "5432"; + var name = dbSettings["Name"] ?? "postgres"; + var username = dbSettings["Username"] ?? "postgres"; + var password = dbSettings["Password"] ?? "postgres"; + x.UseNpgsql($"Server={hostname}:{port};Database={name};Uid={username};Pwd={password};"); + }); + services.AddScoped(sp => new UnitOfWork(sp.GetRequiredService())); + services.AddScoped(typeof(GenericRepository<>)); + return services; + } +} + +public static class SwaggerExtensions +{ + public static IServiceCollection AddSwagger(this IServiceCollection services) + { + string projectName = Assembly.GetExecutingAssembly().GetName().Name; + services.AddOpenApi(); + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = projectName, Version = "v1" }); + + // Set the comments path for the Swagger JSON and UI + var xmlFile = $"{projectName}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + }); + return services; + } +} + +public static class JwtAuthExtensions +{ + public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + var secretKey = jwtSettings["SecretKey"] ?? throw new SystemException("JwtSettings:SecretKey not found"); + var key = Encoding.ASCII.GetBytes(secretKey); + + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); + + return services; + } +} + +public static class BackendServicesExtensions +{ + public static IServiceCollection AddBackendServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} + +public static class UtilServicesExtensions +{ + public static IServiceCollection AddUtilServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; + } +} + +public static class NotificationSettings +{ + public static IServiceCollection AddPushNotifications(this IServiceCollection services, IConfiguration configuration) + { + var notificationSettings = configuration.GetSection("NotificationSettings"); + var apiKey = notificationSettings["ApiKey"]; + var token = notificationSettings["Token"]; + var baseUrl = notificationSettings["Url"]; + var projectId = notificationSettings["ProjectId"]; + + HttpClient client = new HttpClient(); + client.BaseAddress = new Uri(baseUrl); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); + + services.AddSingleton(provider => + { + var logger = provider.GetRequiredService>(); + return new PushNotificationsClient(client, logger, token, projectId); + }); + return services; + } +} +public static class EmailExtensions +{ + public static IServiceCollection AddEmail(this IServiceCollection services, IConfiguration configuration) + { + var smtpSettings = configuration.GetSection("EmailSettings"); + var host = smtpSettings["Host"] ?? "localhost"; + var port = Convert.ToInt32(smtpSettings["Port"] ?? "25"); + var username = smtpSettings["Username"] ?? "username"; + var password = smtpSettings["Password"] ?? "password"; + var email = smtpSettings["EmailFrom"] ?? "email"; + services.AddScoped(sp => new SmtpClient(host) + { + Port = port, + Credentials = new NetworkCredential(username, password), + EnableSsl = true, + }); + + services.AddSingleton(); + return services; + } +} + +public static class FactoryExtensions +{ + public static IServiceCollection AddFactories(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} diff --git a/MDSBackend.csproj b/MDSBackend.csproj new file mode 100755 index 0000000..58379d7 --- /dev/null +++ b/MDSBackend.csproj @@ -0,0 +1,53 @@ + + + + net9.0 + enable + enable + Linux + + + + true + bin\Debug\net9.0\GamificationService.xml + + + + 1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + .dockerignore + + + + + + + + diff --git a/Mapper/MappingProfile.cs b/Mapper/MappingProfile.cs new file mode 100755 index 0000000..eb7b21f --- /dev/null +++ b/Mapper/MappingProfile.cs @@ -0,0 +1,169 @@ +using AutoMapper; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; + +namespace GamificationService.Mapper; + + +public class MappingProfile : Profile +{ + public MappingProfile() + { + #region UserProfileMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.UserId, opt => opt.MapFrom(src => src.UserId)) + .ForMember(x => x.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(x => x.Surname, opt => opt.MapFrom(src => src.Surname)) + .ForMember(x => x.Patronymic, opt => opt.MapFrom(src => src.Patronymic)) + .ForMember(x => x.Birthdate, opt => opt.MapFrom(src => src.Birthdate)) + .ForMember(x => x.Gender, opt => opt.MapFrom(src => src.Gender)) + .ForMember(x => x.ContactEmail, opt => opt.MapFrom(src => src.ContactEmail)) + .ForMember(x => x.ContactPhone, opt => opt.MapFrom(src => src.ContactPhone)) + .ForMember(x => x.ProfilePicture, opt => opt.MapFrom(src => src.ProfilePicture)); + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.UserId, opt => opt.MapFrom(src => src.UserId)) + .ForMember(x => x.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(x => x.Surname, opt => opt.MapFrom(src => src.Surname)) + .ForMember(x => x.Patronymic, opt => opt.MapFrom(src => src.Patronymic)) + .ForMember(x => x.Birthdate, opt => opt.MapFrom(src => src.Birthdate)) + .ForMember(x => x.Gender, opt => opt.MapFrom(src => src.Gender)) + .ForMember(x => x.ContactEmail, opt => opt.MapFrom(src => src.ContactEmail)) + .ForMember(x => x.ContactPhone, opt => opt.MapFrom(src => src.ContactPhone)) + .ForMember(x => x.ProfilePicture, opt => opt.MapFrom(src => src.ProfilePicture)); + + CreateMap() + .ForMember(x => x.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(x => x.Surname, opt => opt.MapFrom(src => src.Surname)) + .ForMember(x => x.Patronymic, opt => opt.MapFrom(src => src.Patronymic)) + .ForMember(x => x.Birthdate, opt => opt.MapFrom(src => src.Birthdate)) + .ForMember(x => x.Gender, opt => opt.MapFrom(src => src.Gender)) + .ForMember(x => x.ContactEmail, opt => opt.MapFrom(src => src.ContactEmail)) + .ForMember(x => x.ContactPhone, opt => opt.MapFrom(src => src.ContactPhone)) + .ForMember(x => x.ProfilePicture, opt => opt.MapFrom(src => src.ProfilePicture)); + + #endregion + + #region InstructionParagraphMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Text, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.InstructionId, opt => opt.MapFrom(src => src.InstructionId)) + .ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order)) + .ForMember(x => x.ImageUrl, opt => opt.MapFrom(src => src.ImageUrl)) + .ForMember(x => x.VideoUrl, opt => opt.MapFrom(src => src.VideoUrl)); + + #endregion + + #region InstructionMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(x => x.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(x => x.CategoryId, opt => opt.MapFrom(src => src.CategoryId)) + .ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.AssignDate)) + .ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.DeadlineDate)) + .ForMember(x => x.IsEnabled, opt => opt.MapFrom(src => src.IsEnabled)) + .ForMember(x => x.Paragraphs, opt => opt.MapFrom(src => src.Paragraphs)); + + #endregion + + #region InstructionCreateMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(x => x.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(x => x.Paragraphs, opt => opt.MapFrom(src => src.Paragraphs)) + .ForMember(x => x.CategoryId, opt => opt.MapFrom(src => src.CategoryId)) + .ForMember(x => x.AssignDate, opt => opt.MapFrom(src => src.AssignDate)) + .ForMember(x => x.DeadlineDate, opt => opt.MapFrom(src => src.DeadlineDate)) + .ForMember(x => x.IsEnabled, opt => opt.MapFrom(src => src.IsEnabled)); + #endregion + + #region InstructionParagraphCreateMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Text, opt => opt.MapFrom(src => src.Text)) + .ForMember(x => x.InstructionId, opt => opt.MapFrom(src => src.InstructionId)) + .ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order)) + .ForMember(x => x.ImageUrl, opt => opt.MapFrom(src => src.ImageUrl)) + .ForMember(x => x.VideoUrl, opt => opt.MapFrom(src => src.VideoUrl)); + + #endregion + + #region InstructionCategoryMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)); + + #endregion + + #region InstructionCategoryCreateMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)); + + #endregion + + #region InstructionTestMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(x => x.Questions, opt => opt.MapFrom(src => src.Questions)) + .ForMember(x => x.ScoreCalcMethod, opt => opt.MapFrom(src => src.ScoreCalcMethod)) + .ForMember(x => x.MinScore, opt => opt.MapFrom(src => src.MinScore)) + .ForMember(x => x.MaxAttempts, opt => opt.MapFrom(src => src.MaxAttempts)); + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(x => x.ScoreCalcMethod, opt => opt.MapFrom(src => src.ScoreCalcMethod)) + .ForMember(x => x.MinScore, opt => opt.MapFrom(src => src.MinScore)) + .ForMember(x => x.MaxAttempts, opt => opt.MapFrom(src => src.MaxAttempts)) + .ForMember(x => x.Questions, opt => opt.MapFrom(src => src.Questions)); + + #endregion + + #region InstructionTestQuestionCreateMapping + + CreateMap() + .ForMember(x => x.Question, opt => opt.MapFrom(src => src.Question)) + .ForMember(x => x.Answers, opt => opt.MapFrom(src => src.Answers)) + .ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order)) + .ForMember(x => x.IsMultipleAnswer, opt => opt.MapFrom(src => src.IsMultipleAnswer)) + .ForMember(x => x.CorrectAnswers, opt => opt.MapFrom(src => src.CorrectAnswers)); + + #endregion + + #region InstructionTestQuestionMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.Question, opt => opt.MapFrom(src => src.Question)) + .ForMember(x => x.Answers, opt => opt.MapFrom(src => src.Answers)) + .ForMember(x => x.Order, opt => opt.MapFrom(src => src.Order)) + .ForMember(x => x.IsMultipleAnswer, opt => opt.MapFrom(src => src.IsMultipleAnswer)); + + #endregion + + #region InstructionTestResultMapping + + CreateMap() + .ForMember(x => x.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(x => x.UserId, opt => opt.MapFrom(src => src.UserId)) + .ForMember(x => x.InstructionTestId, opt => opt.MapFrom(src => src.InstructionTestId)) + .ForMember(x => x.Score, opt => opt.MapFrom(src => src.Score)); + + #endregion + } +} diff --git a/Models/BasicResponses/BasicResponse.cs b/Models/BasicResponses/BasicResponse.cs new file mode 100755 index 0000000..b8f20b5 --- /dev/null +++ b/Models/BasicResponses/BasicResponse.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.BasicResponses; + +public class BasicResponse +{ + public short Code { get; set; } + public string Message { get; set; } +} diff --git a/Models/DTO/AuthDTO.cs b/Models/DTO/AuthDTO.cs new file mode 100755 index 0000000..5acacfc --- /dev/null +++ b/Models/DTO/AuthDTO.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class AuthDTO +{ + [Required] + [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] + public string Username { get; set; } = null!; + + [Required] + [EmailAddress(ErrorMessage = "Invalid email address")] + public string Email { get; set; } = null!; + + [Required] + [StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be between 8 and 100 characters")] + public string Password { get; set; } = null!; + + [Required] + public bool RememberMe { get; set; } +} diff --git a/Models/DTO/DisableTwoFactorDTO.cs b/Models/DTO/DisableTwoFactorDTO.cs new file mode 100644 index 0000000..fee92cc --- /dev/null +++ b/Models/DTO/DisableTwoFactorDTO.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.DTO; + +public class DisableTwoFactorDTO +{ + public int TwoFactorProvider { get; set; } + public string Code { get; set; } +} diff --git a/Models/DTO/EnableTwoFactorDTO.cs b/Models/DTO/EnableTwoFactorDTO.cs new file mode 100644 index 0000000..87fc745 --- /dev/null +++ b/Models/DTO/EnableTwoFactorDTO.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.DTO; + +public class EnableTwoFactorDTO +{ + public int TwoFactorProvider { get; set; } +} diff --git a/Models/DTO/GetAllRightsResponse.cs b/Models/DTO/GetAllRightsResponse.cs new file mode 100644 index 0000000..071b87a --- /dev/null +++ b/Models/DTO/GetAllRightsResponse.cs @@ -0,0 +1,11 @@ +using GamificationService.Models.Database; + +namespace GamificationService.Models.DTO; + +public class GetAllRightsResponse +{ + public List Rights { get; set; } + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } +} diff --git a/Models/DTO/GetAllRolesResponse.cs b/Models/DTO/GetAllRolesResponse.cs new file mode 100644 index 0000000..4dfa166 --- /dev/null +++ b/Models/DTO/GetAllRolesResponse.cs @@ -0,0 +1,11 @@ +using GamificationService.Models.Database; + +namespace GamificationService.Models.DTO; + +public class GetAllRolesResponse +{ + public List Roles { get; set; } + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } +} diff --git a/Models/DTO/GetTwoFactorDTO.cs b/Models/DTO/GetTwoFactorDTO.cs new file mode 100644 index 0000000..bbafc06 --- /dev/null +++ b/Models/DTO/GetTwoFactorDTO.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class GetTwoFactorDTO +{ + [Required] + public int TwoFactorProvider { get; set; } + [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] + public string? Username { get; set; } = null!; +} diff --git a/Models/DTO/InstructionCategoryCreateDTO.cs b/Models/DTO/InstructionCategoryCreateDTO.cs new file mode 100644 index 0000000..aa03952 --- /dev/null +++ b/Models/DTO/InstructionCategoryCreateDTO.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionCategoryCreateDTO +{ + public long? Id { get; set; } + + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; +} + + + diff --git a/Models/DTO/InstructionCategoryDTO.cs b/Models/DTO/InstructionCategoryDTO.cs new file mode 100644 index 0000000..c3482e5 --- /dev/null +++ b/Models/DTO/InstructionCategoryDTO.cs @@ -0,0 +1,10 @@ +namespace GamificationService.Models.DTO; + +public class InstructionCategoryDTO +{ + public long? Id { get; set; } + + public string? Title { get; set; } = null!; +} + + diff --git a/Models/DTO/InstructionCreateDTO.cs b/Models/DTO/InstructionCreateDTO.cs new file mode 100644 index 0000000..4f645f0 --- /dev/null +++ b/Models/DTO/InstructionCreateDTO.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionCreateDTO +{ + public long? Id { get; set; } + + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; + + public string? Description { get; set; } + + [Required(ErrorMessage = "Paragraphs are required")] + public List Paragraphs { get; set; } = null!; + + [Required(ErrorMessage = "Category id is required")] + public long CategoryId { get; set; } + + /// + /// If AssignDate is set, the instruction will be automatically enabled + /// when the date is reached. If it's not set, the test will automatically + /// obtain the current date as its AssignDate as soon as the instruction + /// will be enabled by the IsEnabled parameter. + /// + public DateTime? AssignDate { get; set; } + + /// + /// When deadline is reached, no more submissions are allowed for this instruction. + /// + public DateTime? DeadlineDate { get; set; } + + /// + /// Disabled instructions cannot be seen by users. + /// Tests for such instructions cannot be submitted either. + /// + public bool IsEnabled { get; set; } = false; +} diff --git a/Models/DTO/InstructionDTO.cs b/Models/DTO/InstructionDTO.cs new file mode 100644 index 0000000..04b4ba6 --- /dev/null +++ b/Models/DTO/InstructionDTO.cs @@ -0,0 +1,21 @@ +namespace GamificationService.Models.DTO; + +public class InstructionDTO +{ + public long? Id { get; set; } + + public string? Title { get; set; } + + public string? Description { get; set; } + + public List Paragraphs { get; set; } = new List(); + + public long? CategoryId { get; set; } + + public DateTime? AssignDate { get; set; } + + public DateTime? DeadlineDate { get; set; } + + public bool IsEnabled { get; set; } +} + diff --git a/Models/DTO/InstructionParagraphCreateDTO.cs b/Models/DTO/InstructionParagraphCreateDTO.cs new file mode 100644 index 0000000..10ea234 --- /dev/null +++ b/Models/DTO/InstructionParagraphCreateDTO.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionParagraphCreateDTO +{ + public long? Id { get; set; } + + public long? InstructionId { get; set; } + + /// + /// Order defines the order of the paragraphs inside the instruction. + /// There must not be two paragraphs with the same order. + /// + public int Order { get; set; } + + [Required(ErrorMessage = "Text is required")] + public string Text { get; set; } = null!; + + public string? ImageUrl { get; set; } + + public string? VideoUrl { get; set; } +} + + diff --git a/Models/DTO/InstructionParagraphDTO.cs b/Models/DTO/InstructionParagraphDTO.cs new file mode 100644 index 0000000..29bf0ba --- /dev/null +++ b/Models/DTO/InstructionParagraphDTO.cs @@ -0,0 +1,21 @@ +namespace GamificationService.Models.DTO; + +public class InstructionParagraphDTO +{ + public long? Id { get; set; } + + public long? InstructionId { get; set; } + + /// + /// Order defines the order of the paragraphs inside the instruction. + /// There must not be two paragraphs with the same order. + /// + public int Order { get; set; } + + public string? Text { get; set; } = null!; + + public string? ImageUrl { get; set; } + + public string? VideoUrl { get; set; } +} + diff --git a/Models/DTO/InstructionTestCreateDTO.cs b/Models/DTO/InstructionTestCreateDTO.cs new file mode 100644 index 0000000..b0800a4 --- /dev/null +++ b/Models/DTO/InstructionTestCreateDTO.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.DTO; + +public class InstructionTestCreateDTO +{ + public long? Id { get; set; } + public string? Title { get; set; } + + [Required(ErrorMessage = "Questions must be specified")] + public ICollection Questions { get; set; } = null!; + + public int MaxAttempts { get; set; } = 10; + + [Range(0, 1.0, ErrorMessage = "Minimum score must be between 0.6 and 1.0")] + public double MinScore { get; set; } = 0.6; + + public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade; +} diff --git a/Models/DTO/InstructionTestDTO.cs b/Models/DTO/InstructionTestDTO.cs new file mode 100644 index 0000000..76b0267 --- /dev/null +++ b/Models/DTO/InstructionTestDTO.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.DTO; + +public class InstructionTestDTO +{ + public long? Id { get; set; } + + // Reserved just in case + [StringLength(300, ErrorMessage = "Title cannot be longer than 300 characters")] + public string? Title { get; set; } + + public int MaxAttempts { get; set; } = 10; + + public double MinScore { get; set; } = 0.6; + + public List Questions { get; set; } = new List(); + + public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade; + +} + diff --git a/Models/DTO/InstructionTestQuestionCreateDTO.cs b/Models/DTO/InstructionTestQuestionCreateDTO.cs new file mode 100644 index 0000000..9da71cf --- /dev/null +++ b/Models/DTO/InstructionTestQuestionCreateDTO.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionTestQuestionCreateDTO +{ + public long? Id { get; set; } + + public bool IsMultipleAnswer { get; set; } + + /// + /// Question will be displayed in the paragraph with the same order number. + /// There can be multiple questions attached to the same paragraph. + /// + public int Order { get; set; } + + [Required(ErrorMessage = "Must have question text")] + public string Question { get; set; } = null!; + + [Required(ErrorMessage = "Must have answer options")] + public ICollection Answers { get; set; } = null!; + + [Required(ErrorMessage = "Must have correct answers")] + public ICollection CorrectAnswers { get; set; } = null!; +} + diff --git a/Models/DTO/InstructionTestQuestionDTO.cs b/Models/DTO/InstructionTestQuestionDTO.cs new file mode 100644 index 0000000..fe144bd --- /dev/null +++ b/Models/DTO/InstructionTestQuestionDTO.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionTestQuestionDTO +{ + public long? Id { get; set; } + + public bool IsMultipleAnswer { get; set; } + + /// + /// Question will be displayed in the paragraph with the same order number. + /// There can be multiple questions attached to the same paragraph. + /// + public int Order { get; set; } + + [Required(ErrorMessage = "Must have question text")] + public string Question { get; set; } = null!; + + [Required(ErrorMessage = "Must have answer options")] + public ICollection Answers { get; set; } = null!; + + public ICollection? CorrectAnswers { get; set; } +} + diff --git a/Models/DTO/InstructionTestResultDTO.cs b/Models/DTO/InstructionTestResultDTO.cs new file mode 100644 index 0000000..ceb7277 --- /dev/null +++ b/Models/DTO/InstructionTestResultDTO.cs @@ -0,0 +1,12 @@ +namespace GamificationService.Models.DTO; + +public class InstructionTestResultDTO +{ + public long? Id { get; set; } + + public long? InstructionTestId { get; set; } + + public long? UserId { get; set; } + + public int Score { get; set; } +} diff --git a/Models/DTO/InstructionTestSubmissionDTO.cs b/Models/DTO/InstructionTestSubmissionDTO.cs new file mode 100644 index 0000000..42a3e71 --- /dev/null +++ b/Models/DTO/InstructionTestSubmissionDTO.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class InstructionTestSubmissionDTO +{ + [Required(ErrorMessage = "InstructionTestId is required")] + public int InstructionTestId { get; set; } + + [Required(ErrorMessage = "Answers must be provided")] + public List> Answers { get; set; } = null!; +} diff --git a/Models/DTO/LoginResultResponse.cs b/Models/DTO/LoginResultResponse.cs new file mode 100755 index 0000000..b1236ff --- /dev/null +++ b/Models/DTO/LoginResultResponse.cs @@ -0,0 +1,9 @@ +namespace GamificationService.Models.DTO; + +public class LoginResultResponse +{ + public bool? RequiresTwoFactorAuth { get; set; } + public bool Success { get; set; } + public RefreshTokenDTO? Token { get; set; } + public int? TwoFactorProvider { get; set; } +} diff --git a/Models/DTO/RefreshTokenDTO.cs b/Models/DTO/RefreshTokenDTO.cs new file mode 100755 index 0000000..28f3b83 --- /dev/null +++ b/Models/DTO/RefreshTokenDTO.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.DTO; + +public class RefreshTokenDTO +{ + public string AccessToken { get; set; } = null!; + public string RefreshToken { get; set; } = null!; +} diff --git a/Models/DTO/RightDTO.cs b/Models/DTO/RightDTO.cs new file mode 100644 index 0000000..433ef5c --- /dev/null +++ b/Models/DTO/RightDTO.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.DTO; + +public class RightDTO +{ + public string Name { get; set; } + public string Description { get; set; } +} diff --git a/Models/DTO/RoleDTO.cs b/Models/DTO/RoleDTO.cs new file mode 100644 index 0000000..c1978ad --- /dev/null +++ b/Models/DTO/RoleDTO.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.DTO; + +public class RoleDTO +{ + public string Name { get; set; } + public string Description { get; set; } +} diff --git a/Models/DTO/TwoFactorDTO.cs b/Models/DTO/TwoFactorDTO.cs new file mode 100755 index 0000000..d5601e8 --- /dev/null +++ b/Models/DTO/TwoFactorDTO.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.DTO; + +public class TwoFactorDTO +{ + [Required] + public int TwoFactorProvider { get; set; } + [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] + public string? Username { get; set; } = null!; + + [Required] + [StringLength(6, MinimumLength = 6, ErrorMessage = "Code must be 6 characters long")] + public string Code { get; set; } = null!; + public bool RememberMe { get; set; } +} diff --git a/Models/DTO/UserProfileCreateDTO.cs b/Models/DTO/UserProfileCreateDTO.cs new file mode 100644 index 0000000..307f1a2 --- /dev/null +++ b/Models/DTO/UserProfileCreateDTO.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.DTO; + +public class UserProfileCreateDTO +{ + [Required(ErrorMessage = "Name is required")] + [StringLength(100, ErrorMessage = "Name must be less than 100 characters")] + public string Name { get; set; } = null!; + + [Required(ErrorMessage = "Surname is required")] + [StringLength(100, ErrorMessage = "Surname must be less than 100 characters")] + public string Surname { get; set; } = null!; + + [StringLength(50, ErrorMessage = "Patronymic must be less than 50 characters")] + public string? Patronymic { get; set; } + + [Required(ErrorMessage = "Birthdate is required")] + public DateTime Birthdate { get; set; } + + [Required(ErrorMessage = "Gender is required")] + public Gender Gender { get; set; } + + [EmailAddress(ErrorMessage = "Invalid email")] + public string? ContactEmail { get; set; } + + [Phone(ErrorMessage = "Invalid contact phone number")] + public string? ContactPhone { get; set; } + + [Url(ErrorMessage = "Invalid avatar url")] + public string? ProfilePicture { get; set; } +} diff --git a/Models/DTO/UserProfileDTO.cs b/Models/DTO/UserProfileDTO.cs new file mode 100755 index 0000000..d540605 --- /dev/null +++ b/Models/DTO/UserProfileDTO.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.DTO; + +public class UserProfileDTO +{ + public long? Id { get; set; } + + public long? UserId { get; set; } + + [Required(ErrorMessage = "Name is required")] + [StringLength(100, ErrorMessage = "Name must be less than 100 characters")] + public string Name { get; set; } = null!; + + [Required(ErrorMessage = "Surname is required")] + [StringLength(100, ErrorMessage = "Surname must be less than 100 characters")] + public string Surname { get; set; } = null!; + + [StringLength(50, ErrorMessage = "Patronymic must be less than 50 characters")] + public string? Patronymic { get; set; } + + [Required(ErrorMessage = "Birthdate is required")] + public DateTime Birthdate { get; set; } + + [Required(ErrorMessage = "Gender is required")] + public Gender Gender { get; set; } + + [EmailAddress(ErrorMessage = "Invalid email")] + public string? ContactEmail { get; set; } + + [Phone(ErrorMessage = "Invalid contact phone number")] + public string? ContactPhone { get; set; } + + [Url(ErrorMessage = "Invalid avatar url")] + public string? ProfilePicture { get; set; } +} diff --git a/Models/Database/ApplicationRole.cs b/Models/Database/ApplicationRole.cs new file mode 100755 index 0000000..4d778a8 --- /dev/null +++ b/Models/Database/ApplicationRole.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; +using StackExchange.Redis; + +namespace GamificationService.Models.Database; + + +public class ApplicationRole : IdentityRole +{ + public ApplicationRole() : base() { } + public ApplicationRole(string roleName) : base(roleName) { } + public string? Description { get; set; } + + public List UserRoles { get; set; } = new List(); + public List RoleRights { get; set; } = new List(); +} diff --git a/Models/Database/ApplicationUser.cs b/Models/Database/ApplicationUser.cs new file mode 100755 index 0000000..ab7a7ba --- /dev/null +++ b/Models/Database/ApplicationUser.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils; +using Microsoft.AspNetCore.Identity; + +namespace GamificationService.Models.Database; + +public class ApplicationUser : IdentityUser +{ + [Required(ErrorMessage = "Username is required")] + [StringLength(50, ErrorMessage = "Username must be less than 50 characters")] + public string Username { get; set; } = null!; + public bool TwoFactorEnabled { get; set; } + public string? TwoFactorSecret { get; set; } + public bool EmailConfirmed { get; set; } + public List TwoFactorProviders { get; set; } = new List(); + public List RefreshTokens { get; set; } = new List(); + + public List UserRoles { get; set; } = new List(); +} diff --git a/Models/Database/AuditableEntity.cs b/Models/Database/AuditableEntity.cs new file mode 100755 index 0000000..6c834e6 --- /dev/null +++ b/Models/Database/AuditableEntity.cs @@ -0,0 +1,9 @@ +namespace GamificationService.Models.Database; + +public class AuditableEntity +{ + public DateTimeOffset CreatedOn { get; set; } + public string CreatedBy { get; set; } = string.Empty; + public DateTimeOffset UpdatedOn { get; set; } + public string UpdatedBy { get; set; } = string.Empty; +} diff --git a/Models/Database/Instruction.cs b/Models/Database/Instruction.cs new file mode 100644 index 0000000..ed7f422 --- /dev/null +++ b/Models/Database/Instruction.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class Instruction +{ + [Key] + public long Id { get; set; } + + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; + + public string? Description { get; set; } + + public long CategoryId { get; set; } + [Required(ErrorMessage = "Category must be specified")] + public InstructionCategory Category { get; set; } = null!; + + public virtual ICollection Paragraphs { get; set; } + + public long? InstructionTestId { get; set; } + public InstructionTest? InstructionTest { get; set; } + + public DateTime? AssignDate { get; set; } = DateTime.UtcNow; + + public DateTime? DeadlineDate { get; set; } + + public bool IsEnabled { get; set; } +} diff --git a/Models/Database/InstructionCategory.cs b/Models/Database/InstructionCategory.cs new file mode 100644 index 0000000..ce29bfd --- /dev/null +++ b/Models/Database/InstructionCategory.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class InstructionCategory +{ + [Key] + public long Id { get; set; } + + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; +} + diff --git a/Models/Database/InstructionParagraph.cs b/Models/Database/InstructionParagraph.cs new file mode 100644 index 0000000..59c8be2 --- /dev/null +++ b/Models/Database/InstructionParagraph.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class InstructionParagraph +{ + [Key] + public long Id { get; set; } + + public long InstructionId { get; set; } + [Required(ErrorMessage = "Must be linked to instruction")] + + public int Order { get; set; } + + [Required(ErrorMessage = "Paragraph text is required")] + public string Text { get; set; } = null!; + + public string? ImageUrl { get; set; } + + public string? VideoUrl { get; set; } +} diff --git a/Models/Database/InstructionTest.cs b/Models/Database/InstructionTest.cs new file mode 100644 index 0000000..4b3ea51 --- /dev/null +++ b/Models/Database/InstructionTest.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.Database; + +public class InstructionTest +{ + [Key] + public long Id { get; set; } + + // Reserved just in case + [MaxLength(300, ErrorMessage = "Title cannot be longer than 300 characters")] + public string? Title { get; set; } + + public virtual ICollection Questions { get; set; } + + public int MaxAttempts { get; set; } = 10; + + public double MinScore { get; set; } = 0.6; + + public InstructionTestScoreCalcMethod ScoreCalcMethod { get; set; } = InstructionTestScoreCalcMethod.MaxGrade; + +} diff --git a/Models/Database/InstructionTestQuestion.cs b/Models/Database/InstructionTestQuestion.cs new file mode 100644 index 0000000..44d1ed4 --- /dev/null +++ b/Models/Database/InstructionTestQuestion.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class InstructionTestQuestion +{ + [Key] + public long Id { get; set; } + + [Required(ErrorMessage = "Must be tied to an instruction test")] + public InstructionTest InstructionTest { get; set; } = null!; + public long InstructionTestId { get; set; } + + public int Order { get; set; } + + public bool IsMultipleAnswer { get; set; } + + [Required(ErrorMessage = "Must have question text")] + public string Question { get; set; } = null!; + + [Required(ErrorMessage = "Must have answer options")] + public ICollection Answers { get; set; } = null!; + + [Required(ErrorMessage = "Must have correct answer ids")] + public ICollection CorrectAnswers { get; set; } = null!; +} diff --git a/Models/Database/InstructionTestResult.cs b/Models/Database/InstructionTestResult.cs new file mode 100644 index 0000000..8017553 --- /dev/null +++ b/Models/Database/InstructionTestResult.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class InstructionTestResult +{ + [Key] + public long Id { get; set; } + + public long InstructionTestId { get; set; } + [Required(ErrorMessage = "Instruction test is required")] + public virtual InstructionTest InstructionTest { get; set; } = null!; + + public long UserId { get; set; } + [Required(ErrorMessage = "User is required")] + public ApplicationUser User { get; set; } = null!; + + [Range(0, 100, ErrorMessage = "Score must be a number from 0 to 100")] + public int Score { get; set; } +} diff --git a/Models/Database/RefreshToken.cs b/Models/Database/RefreshToken.cs new file mode 100755 index 0000000..d331dd9 --- /dev/null +++ b/Models/Database/RefreshToken.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class RefreshToken +{ + [Key] + public long Id { get; set; } + + public long UserId { get; set; } + public ApplicationUser User { get; set; } = null!; + + [Required] + public string Token { get; set; } = null!; + + public DateTime Expires { get; set; } + public DateTime Created { get; set; } + public bool IsExpired => DateTime.UtcNow >= Expires; + + public bool IsRevoked { get; set; } + public string? RevokedByIp { get; set; } + public DateTime? RevokedOn { get; set; } + + public bool IsActive => !IsRevoked && !IsExpired; +} diff --git a/Models/Database/Right.cs b/Models/Database/Right.cs new file mode 100755 index 0000000..856f727 --- /dev/null +++ b/Models/Database/Right.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace GamificationService.Models.Database; + +public class Right +{ + [Key] + public long Id { get; set; } + + [Required] + [StringLength(50)] + public string Name { get; set; } = null!; + + [StringLength(100)] + public string? Description { get; set; } + + public List RoleRights { get; set; } = new List(); +} diff --git a/Models/Database/RoleRight.cs b/Models/Database/RoleRight.cs new file mode 100755 index 0000000..80c233e --- /dev/null +++ b/Models/Database/RoleRight.cs @@ -0,0 +1,10 @@ +namespace GamificationService.Models.Database; + +public class RoleRight +{ + public long RoleId { get; set; } + public ApplicationRole Role { get; set; } = null!; + + public long RightId { get; set; } + public Right Right { get; set; } = null!; +} diff --git a/Models/Database/UserProfile.cs b/Models/Database/UserProfile.cs new file mode 100755 index 0000000..1806614 --- /dev/null +++ b/Models/Database/UserProfile.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Utils.Enums; + +namespace GamificationService.Models.Database; + +public class UserProfile +{ + [Key] + public long Id { get; set; } + + [Required(ErrorMessage = "User is required")] + public long UserId { get; set; } + public ApplicationUser? User { get; set; } + + [Required(ErrorMessage = "Name is required")] + [StringLength(100, ErrorMessage = "Name must be less than 100 characters")] + public string Name { get; set; } = null!; + + [Required(ErrorMessage = "Surname is required")] + [StringLength(100, ErrorMessage = "Surname must be less than 100 characters")] + public string Surname { get; set; } = null!; + + [StringLength(50, ErrorMessage = "Patronymic must be less than 50 characters")] + public string? Patronymic { get; set; } + + [Required(ErrorMessage = "Gender is required")] + public Gender Gender { get; set; } + + [Required(ErrorMessage = "Birthdate is required")] + public DateTime Birthdate { get; set; } + + [EmailAddress(ErrorMessage = "Invalid email")] + public string? ContactEmail { get; set; } + + [Phone(ErrorMessage = "Invalid contact phone number")] + public string? ContactPhone { get; set; } + + [Url(ErrorMessage = "Invalid avatar url")] + public string? ProfilePicture { get; set; } +} diff --git a/Models/Database/UserRole.cs b/Models/Database/UserRole.cs new file mode 100755 index 0000000..f725bb1 --- /dev/null +++ b/Models/Database/UserRole.cs @@ -0,0 +1,10 @@ +namespace GamificationService.Models.Database; + +public class UserRole +{ + public long UserId { get; set; } + public ApplicationUser User { get; set; } = null!; + + public long RoleId { get; set; } + public ApplicationRole Role { get; set; } = null!; +} diff --git a/Models/Messages/InstructionTests/CreateInstructionTestRequest.cs b/Models/Messages/InstructionTests/CreateInstructionTestRequest.cs new file mode 100644 index 0000000..1a7256b --- /dev/null +++ b/Models/Messages/InstructionTests/CreateInstructionTestRequest.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.InstructionTests; + +public class CreateInstructionTestRequest +{ + +} diff --git a/Models/Messages/InstructionTests/CreateInstructionTestResponse.cs b/Models/Messages/InstructionTests/CreateInstructionTestResponse.cs new file mode 100644 index 0000000..53b8088 --- /dev/null +++ b/Models/Messages/InstructionTests/CreateInstructionTestResponse.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.InstructionTests; + +public class CreateInstructionTestResponse +{ + +} diff --git a/Models/Messages/InstructionTests/UpdateInstructionTestRequest.cs b/Models/Messages/InstructionTests/UpdateInstructionTestRequest.cs new file mode 100644 index 0000000..7053768 --- /dev/null +++ b/Models/Messages/InstructionTests/UpdateInstructionTestRequest.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.InstructionTests; + +public class UpdateInstructionTestRequest +{ + +} diff --git a/Models/Messages/InstructionTests/UpdateInstructionTestResponse.cs b/Models/Messages/InstructionTests/UpdateInstructionTestResponse.cs new file mode 100644 index 0000000..7552973 --- /dev/null +++ b/Models/Messages/InstructionTests/UpdateInstructionTestResponse.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.InstructionTests; + +public class UpdateInstructionTestResponse +{ + +} diff --git a/Models/Messages/Instructions/CreateInstructionRequest.cs b/Models/Messages/Instructions/CreateInstructionRequest.cs new file mode 100644 index 0000000..d32445c --- /dev/null +++ b/Models/Messages/Instructions/CreateInstructionRequest.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Models.DTO; + +namespace GamificationService.Models.Messages.Instructions; + +public class CreateInstructionRequest +{ + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; + + public string? Description { get; set; } + + [Required(ErrorMessage = "Paragraphs are required")] + public List Paragraphs { get; set; } = null!; + + [Required(ErrorMessage = "Category id is required")] + public long CategoryId { get; set; } + + /// + /// If AssignDate is set, the instruction will be automatically enabled + /// when the date is reached. If it's not set, the test will automatically + /// obtain the current date as its AssignDate as soon as the instruction + /// will be enabled by the IsEnabled parameter. + /// + public DateTime? AssignDate { get; set; } + + /// + /// When deadline is reached, no more submissions are allowed for this instruction. + /// + public DateTime? DeadlineDate { get; set; } + + /// + /// Disabled instructions cannot be seen by users. + /// Tests for such instructions cannot be submitted either. + /// + public bool IsEnabled { get; set; } = false; +} diff --git a/Models/Messages/Instructions/CreateInstructionResponse.cs b/Models/Messages/Instructions/CreateInstructionResponse.cs new file mode 100644 index 0000000..dd73918 --- /dev/null +++ b/Models/Messages/Instructions/CreateInstructionResponse.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.Instructions; + +public class CreateInstructionResponse +{ + +} diff --git a/Models/Messages/Instructions/UpdateInstructionRequest.cs b/Models/Messages/Instructions/UpdateInstructionRequest.cs new file mode 100644 index 0000000..32b5df8 --- /dev/null +++ b/Models/Messages/Instructions/UpdateInstructionRequest.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using GamificationService.Models.DTO; + +namespace GamificationService.Models.Messages.Instructions; + +public class UpdateInstructionRequest +{ + public long Id { get; set; } + + [Required(ErrorMessage = "Title is required")] + public string Title { get; set; } = null!; + + public string? Description { get; set; } + + [Required(ErrorMessage = "Paragraphs are required")] + public List Paragraphs { get; set; } = null!; + + [Required(ErrorMessage = "Category id is required")] + public long CategoryId { get; set; } + + /// + /// If AssignDate is set, the instruction will be automatically enabled + /// when the date is reached. If it's not set, the test will automatically + /// obtain the current date as its AssignDate as soon as the instruction + /// will be enabled by the IsEnabled parameter. + /// + public DateTime? AssignDate { get; set; } + + /// + /// When deadline is reached, no more submissions are allowed for this instruction. + /// + public DateTime? DeadlineDate { get; set; } + + /// + /// Disabled instructions cannot be seen by users. + /// Tests for such instructions cannot be submitted either. + /// + public bool IsEnabled { get; set; } = false; +} + diff --git a/Models/Messages/Instructions/UpdateInstructionResponse.cs b/Models/Messages/Instructions/UpdateInstructionResponse.cs new file mode 100644 index 0000000..e58c5ed --- /dev/null +++ b/Models/Messages/Instructions/UpdateInstructionResponse.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.Instructions; + +public class UpdateInstructionResponse +{ + +} diff --git a/Models/Messages/UserProfiles/CreateUserProfileRequest.cs b/Models/Messages/UserProfiles/CreateUserProfileRequest.cs new file mode 100644 index 0000000..aec4dbf --- /dev/null +++ b/Models/Messages/UserProfiles/CreateUserProfileRequest.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.UserProfiles; + +public class CreateUserProfileRequest +{ + +} diff --git a/Models/Messages/UserProfiles/CreateUserProfileResponse.cs b/Models/Messages/UserProfiles/CreateUserProfileResponse.cs new file mode 100644 index 0000000..ddb8dbd --- /dev/null +++ b/Models/Messages/UserProfiles/CreateUserProfileResponse.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models.Messages.UserProfiles; + +public class CreateUserProfileResponse +{ + +} + diff --git a/Models/Messages/UserProfiles/UpdateUserProfileRequest.cs b/Models/Messages/UserProfiles/UpdateUserProfileRequest.cs new file mode 100644 index 0000000..c31c771 --- /dev/null +++ b/Models/Messages/UserProfiles/UpdateUserProfileRequest.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.UserProfiles; + +public class UpdateUserProfileRequest +{ + +} diff --git a/Models/Messages/UserProfiles/UpdateUserProfileResponse.cs b/Models/Messages/UserProfiles/UpdateUserProfileResponse.cs new file mode 100644 index 0000000..cefff9f --- /dev/null +++ b/Models/Messages/UserProfiles/UpdateUserProfileResponse.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Models.Messages.UserProfiles; + +public class UpdateUserProfileResponse +{ + +} diff --git a/Models/UserSession.cs b/Models/UserSession.cs new file mode 100755 index 0000000..fbbe5c7 --- /dev/null +++ b/Models/UserSession.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Models; + +public class UserSession +{ + public string? Login { get; set; } + public bool IsAuthenticated { get; set; } +} diff --git a/Program.cs b/Program.cs new file mode 100755 index 0000000..c759c1a --- /dev/null +++ b/Program.cs @@ -0,0 +1,83 @@ +using GamificationService.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddHttpContextAccessor(); + +#region Factories + +builder.Services.AddFactories(); + +#endregion + +#region Mapping + +builder.Services.AddMapping(); + +#endregion + +#region Caching + +builder.Services.AddRedisCaching(builder.Configuration); + +#endregion + +#region Logging + +builder.Services.AddLogging(); + +#endregion + + +#region SMTP + +builder.Services.AddEmail(builder.Configuration); + +#endregion + +#region Database + +builder.Services.AddDatabase(builder.Configuration); + +#endregion + + +#region JwtAuth + +builder.Services.AddJwtAuth(builder.Configuration); + +#endregion + +#region UtilServices + +builder.Services.AddUtilServices(); + +#endregion + +#region Services + +builder.Services.AddBackendServices(); + +#endregion + +builder.Services.AddControllers(); +builder.Services.AddSwagger(); + +var app = builder.Build(); + +app.MapOpenApi(); + +app.UseSwagger(); +app.UseSwaggerUI(c => +{ + c.SwaggerEndpoint("/swagger/v1/swagger.json", "LCT2025 GamificationService API V1"); + c.RoutePrefix = string.Empty; +}); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100755 index 0000000..8c97dd8 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5273", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7193;http://localhost:5273", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Services/Cookies/CookieService.cs b/Services/Cookies/CookieService.cs new file mode 100755 index 0000000..a7a3bbf --- /dev/null +++ b/Services/Cookies/CookieService.cs @@ -0,0 +1,47 @@ +using GamificationService.Exceptions.UtilServices.Cookies; + +namespace GamificationService.Services.Cookies; + +public class CookieService : ICookieService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public CookieService(IHttpContextAccessor httpContextAccessor, ILogger logger, IConfiguration configuration) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + _configuration = configuration; + } + + public Task SetCookie(string key, string value, CookieOptions options) + { + try + { + _logger.LogDebug("Adding cookie {CookieKey} with value {CookieValue}", key, value); + _httpContextAccessor.HttpContext.Response.Cookies.Append(key, value, options); + return Task.FromResult(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add cookie {CookieKey}", key); + throw new SetCookiesException(ex.Message); + } + } + + public async Task RemoveCookie(string key) + { + try + { + _logger.LogDebug("Deleting cookie {CookieKey}", key); + _httpContextAccessor.HttpContext.Response.Cookies.Delete(key); + return await Task.FromResult(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete cookie {CookieKey}", key); + throw new DeleteCookiesException(ex.Message); + } + } +} diff --git a/Services/Cookies/ICookieService.cs b/Services/Cookies/ICookieService.cs new file mode 100755 index 0000000..cecfe0d --- /dev/null +++ b/Services/Cookies/ICookieService.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Services.Cookies; + +public interface ICookieService +{ + Task SetCookie(string key, string value, CookieOptions options); + Task RemoveCookie(string key); +} diff --git a/Services/CurrentUsers/CurrentUserService.cs b/Services/CurrentUsers/CurrentUserService.cs new file mode 100644 index 0000000..e1ab572 --- /dev/null +++ b/Services/CurrentUsers/CurrentUserService.cs @@ -0,0 +1,26 @@ +using GamificationService.Models; + +namespace GamificationService.Services.CurrentUsers; + +public class CurrentUserService : ICurrentUserService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + public CurrentUserService(IHttpContextAccessor httpContextAccessor, ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + } + + public UserSession GetCurrentUser() + { + UserSession currentUser = new UserSession + { + IsAuthenticated = _httpContextAccessor.HttpContext.User.Identity != null && _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated, + Login = _httpContextAccessor.HttpContext.User.Identity.Name + }; + _logger.LogDebug($"Current user extracted: {currentUser.Login}"); + return currentUser; + } + +} diff --git a/Services/CurrentUsers/ICurrentUserService.cs b/Services/CurrentUsers/ICurrentUserService.cs new file mode 100644 index 0000000..a71c6de --- /dev/null +++ b/Services/CurrentUsers/ICurrentUserService.cs @@ -0,0 +1,8 @@ +using GamificationService.Models; + +namespace GamificationService.Services.CurrentUsers; + +public interface ICurrentUserService +{ + UserSession GetCurrentUser(); +} diff --git a/Services/InstructionTests/IInstructionTestsService.cs b/Services/InstructionTests/IInstructionTestsService.cs new file mode 100644 index 0000000..c06939b --- /dev/null +++ b/Services/InstructionTests/IInstructionTestsService.cs @@ -0,0 +1,22 @@ +using GamificationService.Models.Database; +using GamificationService.Models.DTO; + +namespace GamificationService.Services.InstructionTests; + +public interface IInstructionTestsService +{ + public Task CreateInstructionTest(InstructionTest instructionTest); + public Task CreateInstructionTest(InstructionTestCreateDTO instructionTest); + public Task UpdateInstructionTest(InstructionTest instructionTest); + public Task UpdateInstructionTest(InstructionTestCreateDTO instructionTest); + public Task DeleteInstructionTestByIdAsync(long id); + + public Task SubmitInstructionTestAsync(long userId, InstructionTestSubmissionDTO submission); + + public InstructionTestDTO GetInstructionTestById(long id); + public List GetInstructionTestsByInstructionId(long instructionId); + public List GetInstructionTestQuestionsByInstructionTestId(long instructionTestId); + public List GetUserInstructionTestResultsByInstructionTestId(long userId, long instructionId); + public List GetInstructionTestResultsByUserId(long userId); + public List GetCompletedInstructionTestsByUserId(long userId); +} diff --git a/Services/InstructionTests/InstructionTestsService.cs b/Services/InstructionTests/InstructionTestsService.cs new file mode 100644 index 0000000..c102111 --- /dev/null +++ b/Services/InstructionTests/InstructionTestsService.cs @@ -0,0 +1,294 @@ +using AutoMapper; +using GamificationService.Database.Repositories; +using GamificationService.Exceptions.Services.Instruction; +using GamificationService.Exceptions.Services.InstructionTest; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Utils.Enums; + +namespace GamificationService.Services.InstructionTests; + +public class InstructionTestsService : IInstructionTestsService +{ + private readonly ILogger _logger; + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public InstructionTestsService(ILogger logger, UnitOfWork unitOfWork, IMapper mapper) + { + _logger = logger; + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task CreateInstructionTest(InstructionTestCreateDTO instructionTest) + { + return _mapper.Map(await CreateInstructionTest(_mapper.Map(instructionTest))); + } + + public async Task DeleteInstructionTestByIdAsync(long id) + { + var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(id); + + if (instructionTest == null) + { + _logger.LogError("Instruction test with id {Id} not found", id); + throw new InstructionTestNotFoundException(); + } + + // Find all questions + var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == id); + + // Start transaction + await _unitOfWork.BeginTransactionAsync(); + + foreach (var question in questions) + { + _unitOfWork.InstructionTestQuestionRepository.Delete(question); + } + + // Delete instruction test + _unitOfWork.InstructionTestRepository.Delete(instructionTest); + + if (await _unitOfWork.SaveAsync()) + { + await _unitOfWork.CommitAsync(); + _logger.LogInformation("Instruction test deleted ({Id})", id); + return true; + } + else + { + _logger.LogError("Failed to delete instruction test ({Id})", id); + throw new InstructionTestDeletionException(); + } + + } + + public List GetCompletedInstructionTestsByUserId(long userId) + { + var userTestAttempts = _unitOfWork.InstructionTestResultRepository.Get( + q => q.UserId == userId).ToList(); + + var userInstructionTests = _unitOfWork.InstructionTestRepository.Get( + q => userTestAttempts.Any(a => a.InstructionTestId == q.Id)).ToList(); + + var conclusiveUserTestResults = new List(); + foreach (var instructionTest in userInstructionTests) + { + var scoreCalcMethod = instructionTest.ScoreCalcMethod; + int maxScore = 0; + + if (scoreCalcMethod == InstructionTestScoreCalcMethod.AverageGrade) + { + maxScore = (int)Math.Round(userTestAttempts.Where(q => q.InstructionTestId == instructionTest.Id).Average(q => q.Score)); + } + else + { + maxScore = userTestAttempts.Where(q => q.InstructionTestId == instructionTest.Id).Max(q => q.Score); + } + + if (maxScore >= instructionTest.MinScore) + { + conclusiveUserTestResults.Add(_mapper.Map(userTestAttempts.First(q => q.InstructionTestId == instructionTest.Id))); + } + } + + return conclusiveUserTestResults; + } + + public InstructionTestDTO GetInstructionTestById(long id) + { + var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(id); + if (instructionTest == null) + { + _logger.LogError("Instruction test with id {Id} not found", id); + throw new InstructionTestNotFoundException(); + } + return _mapper.Map(instructionTest); + } + + public List GetInstructionTestQuestionsByInstructionTestId(long instructionTestId) + { + var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == instructionTestId); + return _mapper.Map>(questions); + } + + public List GetUserInstructionTestResultsByInstructionTestId(long userId, long instructionTestId) + { + var userTestResults = _unitOfWork.InstructionTestResultRepository.Get( + q => q.UserId == userId && q.InstructionTestId == instructionTestId).ToList(); + return _mapper.Map>(userTestResults); + } + + public List GetInstructionTestResultsByUserId(long userId) + { + var userTestResults = _unitOfWork.InstructionTestResultRepository.Get( + q => q.UserId == userId).ToList(); + return _mapper.Map>(userTestResults); + } + + public List GetInstructionTestsByInstructionId(long instructionId) + { + var instructionTest = (_unitOfWork.InstructionRepository.GetByID(instructionId) + ?? throw new InstructionNotFoundException()) + .InstructionTest ?? throw new InstructionTestNotFoundException(); + + return _mapper.Map>(new List() {instructionTest}); + } + + public async Task SubmitInstructionTestAsync(long userId, InstructionTestSubmissionDTO submission) + { + // Retrieve the test and questions + var instructionTest = _unitOfWork.InstructionTestRepository.GetByID(submission.InstructionTestId); + + if (instructionTest == null) + { + _logger.LogError("Instruction test with id {Id} not found", submission.InstructionTestId); + throw new InstructionTestNotFoundException(); + } + + // Check remaining attempts + var userTestAttempts = _unitOfWork.InstructionTestResultRepository.Get( + q => q.UserId == userId && q.InstructionTestId == submission.InstructionTestId).ToList(); + + if (userTestAttempts.Count >= instructionTest.MaxAttempts) + { + _logger.LogWarning("User {UserId}: denied submission for test {InstructionTestId}: max attempts reached", userId, submission.InstructionTestId); + throw new InstructionTestSubmissionException(); + } + + var questions = _unitOfWork.InstructionTestQuestionRepository.Get(q => q.InstructionTestId == submission.InstructionTestId).ToList(); + + // Verify answers amount + if (questions.Count != submission.Answers.Count) + { + _logger.LogWarning("User {UserId}: denied submission for test {InstructionTestId}: wrong number of answers", userId, submission.InstructionTestId); + throw new InstructionTestSubmissionException(); + } + + // Evaluate answers + double score = 0; + int maxErrorPerQuestion = 1; + for (int i = 0; i < questions.Count; i++) + { + var question = questions[i]; + + // User answers for the question without duplicate options + var answer = submission.Answers[i].Distinct(); + + if (question.IsMultipleAnswer) + { + int correctUserAnswersCount = 0; + int incorrectUserAnswersCount = 0; + int correctAnswersCount = question.CorrectAnswers.Count; + + foreach (var option in answer) + { + if (question.CorrectAnswers.Contains(option)) + { + correctUserAnswersCount++; + } + else + { + incorrectUserAnswersCount++; + } + } + + if (incorrectUserAnswersCount > maxErrorPerQuestion || correctUserAnswersCount == 0) + { + // Nothing scored for the question + continue; + } + + // One question is worth 1 point max + double questionScore = correctUserAnswersCount / (double)correctAnswersCount; + + // Add the question score, or half of it if an error is present + score += incorrectUserAnswersCount > 0 ? questionScore /= 2 : questionScore; + } + else + { + score += question.CorrectAnswers.Contains(answer.First()) ? 1 : 0; + } + } + + score = Math.Round(score / questions.Count)*100; + + // Add test result + await _unitOfWork.BeginTransactionAsync(); + + InstructionTestResult newTestResult = new InstructionTestResult() + { + UserId = userId, + InstructionTestId = submission.InstructionTestId, + Score = (int)score + }; + + _unitOfWork.InstructionTestResultRepository.Insert(newTestResult); + + if (!await _unitOfWork.SaveAsync()) + { + _logger.LogError("Failed to save test result for user {UserId} and test {InstructionTestId}", userId, submission.InstructionTestId); + throw new InstructionTestSubmissionException(); + } + + await _unitOfWork.CommitAsync(); + return _mapper.Map(newTestResult); + } + + public async Task UpdateInstructionTest(InstructionTestCreateDTO instructionTest) + { + return await UpdateInstructionTest(_mapper.Map(instructionTest)); + } + + public async Task CreateInstructionTest(InstructionTest instructionTest) + { + instructionTest.Id = 0; + + await _unitOfWork.BeginTransactionAsync(); + + await _unitOfWork.InstructionTestRepository.InsertAsync(instructionTest); + + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError("Failure to create instruction test"); + throw new InstructionTestCreationException(); + } + + await _unitOfWork.CommitAsync(); + + _logger.LogInformation($"Created instruction test {instructionTest.Id}"); + return instructionTest; + } + + public async Task UpdateInstructionTest(InstructionTest instructionTest) + { + var existingInstructionTest = _unitOfWork.InstructionTestRepository.GetByID(instructionTest.Id); + if (existingInstructionTest == null) + { + throw new InstructionTestNotFoundException(); + } + + await _unitOfWork.BeginTransactionAsync(); + + _unitOfWork.InstructionTestQuestionRepository.DeleteRange(existingInstructionTest.Questions); + + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Failure to create existing questions for instruction test {instructionTest.Id} during update"); + throw new InstructionTestCreationException(); + } + + _unitOfWork.InstructionTestRepository.Update(instructionTest); + + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Failure to update instruction test {instructionTest.Id}"); + } + + await _unitOfWork.CommitAsync(); + + return true; + } +} + diff --git a/Services/Instructions/IInstructionService.cs b/Services/Instructions/IInstructionService.cs new file mode 100644 index 0000000..6db499c --- /dev/null +++ b/Services/Instructions/IInstructionService.cs @@ -0,0 +1,21 @@ +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Models.Messages.Instructions; + +namespace GamificationService.Services.Instructions; + +public interface IInstructionService +{ + public Task CreateInstruction(Instruction model); + public Task UpdateInstructionById(Instruction model); + public Task DeleteInstructionById(long instructionId); + + public Task CreateInstruction(CreateInstructionRequest model); + public Task UpdateInstructionById(UpdateInstructionRequest model); + + public List GetAllInstructions(long userId); + public List GetInstructionsByCategoryId(long userId, long categoryId); + public List GetCompletedInstructions(long userId); + public List GetUnfinishedInstructions(long userId); + public List GetInstructionById(long userId, long instructionId); +} diff --git a/Services/Instructions/InstructionService.cs b/Services/Instructions/InstructionService.cs new file mode 100644 index 0000000..8fd56f1 --- /dev/null +++ b/Services/Instructions/InstructionService.cs @@ -0,0 +1,174 @@ +using AutoMapper; +using GamificationService.Database.Repositories; +using GamificationService.Exceptions.Services.Instruction; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using GamificationService.Models.Messages.Instructions; +using GamificationService.Services.InstructionTests; + +namespace GamificationService.Services.Instructions; + +public class InstructionService : IInstructionService +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + private readonly ILogger _logger; + private readonly IInstructionTestsService _instructionTestService; + + public InstructionService(UnitOfWork unitOfWork, IMapper mapper, ILogger logger, IInstructionTestsService instructionTestService) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + _logger = logger; + _instructionTestService = instructionTestService; + } + + public async Task CreateInstruction(Instruction model) + { + model.Id = 0; + + await _unitOfWork.BeginTransactionAsync(); + + await _unitOfWork.InstructionRepository.InsertAsync(model); + + if(await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Instruction {model.Id} could not be created"); + throw new InstructionCreationException(); + } + + await _unitOfWork.CommitAsync(); + + _logger.LogInformation($"Instruction {model.Id} created"); + return model; + } + + public async Task CreateInstruction(CreateInstructionRequest model) + { + Instruction newInstruction = new Instruction() + { + Title = model.Title, + Description = model.Description, + CategoryId = model.CategoryId, + Paragraphs = _mapper.Map>(model.Paragraphs), + AssignDate = model.AssignDate, + DeadlineDate = model.DeadlineDate, + IsEnabled = model.IsEnabled + }; + + return _mapper.Map(await CreateInstruction(newInstruction)); + } + + public async Task DeleteInstructionById(long instructionId) + { + Instruction? instruction = _unitOfWork.InstructionRepository.GetByID(instructionId); + if (instruction == null) + { + throw new InstructionNotFoundException(); + } + _unitOfWork.InstructionRepository.Delete(instruction); + + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Instruction {instructionId} could not be deleted"); + throw new InstructionDeletionException(); + } + + _logger.LogInformation($"Instruction {instructionId} deleted"); + return true; + } + + public List GetAllInstructions(long userId) + { + // TODO: select accessible only + var instructions = _unitOfWork.InstructionRepository.Get().ToList(); + return _mapper.Map>(instructions); + } + + public List GetCompletedInstructions(long userId) + { + var completedTests = _instructionTestService.GetCompletedInstructionTestsByUserId(userId); + var instructions = _unitOfWork.InstructionRepository.Get() + .Where(i => completedTests.Any(t => i.InstructionTestId == t.Id)); + + return _mapper.Map>(instructions); + } + + public List GetInstructionById(long userId, long instructionId) + { + // TODO: select accessible only + var instruction = _unitOfWork.InstructionRepository.GetByID(instructionId); + return _mapper.Map>(instruction); + } + + public List GetInstructionsByCategoryId(long userId, long categoryId) + { + var category = _unitOfWork.InstructionCategoryRepository.GetByID(categoryId); + if (category == null) + { + throw new CategoryNotFoundException(); + } + + var instructions = _unitOfWork.InstructionRepository.Get().Where(i => i.CategoryId == categoryId); + return _mapper.Map>(instructions); + } + + public List GetUnfinishedInstructions(long userId) + { + // TODO: only show accessible + var completedTests = _instructionTestService.GetCompletedInstructionTestsByUserId(userId); + + var instructions = _unitOfWork.InstructionRepository.Get() + .Where(i => !completedTests.Any(t => i.InstructionTestId == t.Id)); + + return _mapper.Map>(instructions); + } + + public async Task UpdateInstructionById(Instruction model) + { + var existingInstruction = _unitOfWork.InstructionRepository.GetByID(model.Id); + if (existingInstruction == null) + { + throw new InstructionNotFoundException(); + } + + await _unitOfWork.BeginTransactionAsync(); + + _unitOfWork.InstructionParagraphRepository.DeleteRange(existingInstruction.Paragraphs); + + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Failure to delete older paragraphs for instruction {model.Id} during update"); + throw new InstructionUpdateException(); + } + + _unitOfWork.InstructionRepository.Update(model); + if (await _unitOfWork.SaveAsync() == false) + { + _logger.LogError($"Instruction {model.Id} could not be updated"); + throw new InstructionUpdateException(); + } + _logger.LogInformation($"Instruction {model.Id} updated"); + return true; + } + + public async Task UpdateInstructionById(UpdateInstructionRequest model) + { + Instruction? existingInstruction = _unitOfWork.InstructionRepository.GetByID(model.Id); + + if (existingInstruction == null) + { + throw new InstructionNotFoundException(); + } + + existingInstruction.Title = model.Title; + existingInstruction.Description = model.Description; + existingInstruction.CategoryId = model.CategoryId; + existingInstruction.Paragraphs = _mapper.Map>(model.Paragraphs); + existingInstruction.AssignDate = model.AssignDate; + existingInstruction.DeadlineDate = model.DeadlineDate; + existingInstruction.IsEnabled = model.IsEnabled; + + return await UpdateInstructionById(_mapper.Map(model)); + } +} diff --git a/Services/JWT/IJWTService.cs b/Services/JWT/IJWTService.cs new file mode 100755 index 0000000..917397e --- /dev/null +++ b/Services/JWT/IJWTService.cs @@ -0,0 +1,13 @@ +using System.IdentityModel.Tokens.Jwt; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; + +namespace GamificationService.Services.JWT; + +public interface IJwtService +{ + string GenerateAccessToken(ApplicationUser user); + JwtSecurityToken ValidateAccessToken(string token); + Task GenerateRefreshTokenAsync(ApplicationUser user); + Task RevokeRefreshTokenAsync(long userId, string refreshToken, string remoteIpAddress); +} diff --git a/Services/JWT/JWTService.cs b/Services/JWT/JWTService.cs new file mode 100755 index 0000000..0e85597 --- /dev/null +++ b/Services/JWT/JWTService.cs @@ -0,0 +1,144 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using GamificationService.Database.Repositories; +using GamificationService.Exceptions.Services.JwtService; +using GamificationService.Exceptions.UtilServices.JWT; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; + +namespace GamificationService.Services.JWT; + +public class JwtService : IJwtService +{ + #region Fields + + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly UnitOfWork _unitOfWork; + + #endregion + + + public JwtService(IConfiguration configuration, ILogger logger, UnitOfWork unitOfWork) + { + _configuration = configuration; + _logger = logger; + _unitOfWork = unitOfWork; + } + + public string GenerateAccessToken(ApplicationUser user) + { + var jwtSettings = _configuration.GetSection("JwtSettings"); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])); + var issuer = jwtSettings["Issuer"]; + var audience = jwtSettings["Audience"]; + + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var userRoles = _unitOfWork.UserRoleRepository.Get() + .Where(ur => ur.UserId == user.Id) + .Select(ur => ur.Role) + .Include(rr => rr.RoleRights) + .ThenInclude(rr=>rr.Right) + .ToList(); + + foreach (var role in userRoles) + { + claims.Add(new Claim(ClaimTypes.Role, role.Name)); + + foreach (var right in role.RoleRights.Select(rr => rr.Right)) + { + claims.Add(new Claim("Right", right.Name)); + } + } + + var expires = DateTime.UtcNow.AddMinutes(double.Parse(jwtSettings["AccessTokenExpirationMinutes"])); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: expires, + signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + private string GenerateRefreshToken() + { + var randomNumber = new byte[32]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + + public JwtSecurityToken ValidateAccessToken(string token) + { + var jwtSettings = _configuration.GetSection("JwtSettings"); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])); + var issuer = jwtSettings["Issuer"]; + var audience = jwtSettings["Audience"]; + + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = true, + ValidIssuer = issuer, + ValidateAudience = true, + ValidAudience = audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + SecurityToken validatedToken; + var principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken); + + return validatedToken as JwtSecurityToken; + } + + public async Task GenerateRefreshTokenAsync(ApplicationUser user) + { + var dbRefreshToken = new RefreshToken + { + UserId = user.Id, + Token = GenerateRefreshToken(), + Expires = DateTime.UtcNow.AddDays(double.Parse(_configuration["JwtSettings:RefreshTokenExpirationDays"])), + Created = DateTime.UtcNow + }; + + await _unitOfWork.RefreshTokenRepository.InsertAsync(dbRefreshToken); + if (!await _unitOfWork.SaveAsync()) + { + throw new GenerateRefreshTokenException("Failed to generate refresh token"); + } + + return dbRefreshToken; + } + + public async Task RevokeRefreshTokenAsync(long userId, string refreshToken, string remoteIpAddress) + { + var token = await _unitOfWork.RefreshTokenRepository.Get() + .FirstOrDefaultAsync(x => x.UserId == userId && x.Token == refreshToken); + + if (token != null) + { + token.IsRevoked = true; + token.RevokedByIp = remoteIpAddress; + token.RevokedOn = DateTime.UtcNow; + + await _unitOfWork.SaveAsync(); + } + } +} diff --git a/Services/Notification/INotificationService.cs b/Services/Notification/INotificationService.cs new file mode 100755 index 0000000..d1f8942 --- /dev/null +++ b/Services/Notification/INotificationService.cs @@ -0,0 +1,11 @@ +using System.Net.Mail; +using GamificationService.Models.Database; +using GamificationService.Utils; + +namespace GamificationService.Services.NotificationService; + +public interface INotificationService +{ + public Task SendMailNotificationAsync(ApplicationUser user, Notification notification); + public Task SendPushNotificationAsync(ApplicationUser user, Notification notification); +} diff --git a/Services/Notification/NotificationService.cs b/Services/Notification/NotificationService.cs new file mode 100755 index 0000000..7c56a4e --- /dev/null +++ b/Services/Notification/NotificationService.cs @@ -0,0 +1,56 @@ +using System.Net.Mail; +using GamificationService.Models.Database; +using GamificationService.Utils; +using GamificationService.Utils.Factory; + +namespace GamificationService.Services.NotificationService; + +public class NotificationService : INotificationService +{ + #region Services + + private readonly EmailClient _emailClient; + private readonly ILogger _logger; + private readonly PushNotificationsClient _pushNotificationsClient; + + #endregion + + #region Constructor + + public NotificationService(EmailClient emailClient, PushNotificationsClient pushNotificationsClient, ILogger logger) + { + _emailClient = emailClient; + _pushNotificationsClient = pushNotificationsClient; + _logger = logger; + } + + #endregion + + + public async Task SendMailNotificationAsync(ApplicationUser user, Notification notification) + { + try + { + await _emailClient.SendEmail(((MailNotification)notification).ConvertToMailMessage(), user.Email); + } + catch (Exception e) + { + _logger.LogError(e,e.Message); + throw; + } + } + + //TODO: Refactor, add reg.ru notifications + public async Task SendPushNotificationAsync(ApplicationUser user, Notification notification) + { + try + { + await _emailClient.SendEmail(((MailNotification)notification).ConvertToMailMessage(), user.Email); + } + catch (Exception e) + { + _logger.LogError(e,e.Message); + throw; + } + } +} diff --git a/Services/Rights/IRightsService.cs b/Services/Rights/IRightsService.cs new file mode 100755 index 0000000..dde6c7e --- /dev/null +++ b/Services/Rights/IRightsService.cs @@ -0,0 +1,12 @@ +using GamificationService.Models.Database; + +namespace GamificationService.Services.Rights; + +public interface IRightsService +{ + Task CreateRightAsync(string rightName, string description); + Task UpdateRightAsync(long rightId, string newRightName, string newDescription); + Task DeleteRightAsync(long rightId); + Task GetRightByIdAsync(long rightId); + Task<(List Rights, int TotalCount)> GetAllRightsAsync(int pageNumber = 1, int pageSize = 10); +} diff --git a/Services/Rights/RightsService.cs b/Services/Rights/RightsService.cs new file mode 100755 index 0000000..28b94dc --- /dev/null +++ b/Services/Rights/RightsService.cs @@ -0,0 +1,103 @@ +using GamificationService.Database.Repositories; +using GamificationService.Models.Database; +using Microsoft.EntityFrameworkCore; + +namespace GamificationService.Services.Rights; + +public class RightsService : IRightsService +{ + #region Fields + + private readonly UnitOfWork _unitOfWork; + private readonly ILogger _logger; + + #endregion + + #region Constructor + + public RightsService(UnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + #endregion + + #region Methods + + public async Task CreateRightAsync(string rightName, string description) + { + var right = new Right + { + Name = rightName, + Description = description + }; + + await _unitOfWork.RightRepository.InsertAsync(right); + if (await _unitOfWork.SaveAsync()) + { + return right; + } + + throw new Exception($"Unable to create right for {rightName}"); + } + + public async Task UpdateRightAsync(long rightId, string newRightName, string newDescription) + { + var right = await _unitOfWork.RightRepository.GetByIDAsync(rightId); + + if (right == null) + { + throw new KeyNotFoundException($"Right with ID {rightId} not found"); + } + + right.Name = newRightName; + right.Description = newDescription; + + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception($"Unable to create right for {rightId}"); + } + + return true; + } + + public async Task DeleteRightAsync(long rightId) + { + var right = await _unitOfWork.RightRepository.GetByIDAsync(rightId); + + if (right == null) + { + throw new KeyNotFoundException($"Right with ID {rightId} not found"); + } + + _unitOfWork.RightRepository.Delete(right); + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception($"Unable to delete right for {rightId}"); + } + + return true; + } + + public async Task GetRightByIdAsync(long rightId) + { + return await _unitOfWork.RightRepository.GetByIDAsync(rightId); + } + + public async Task<(List Rights, int TotalCount)> GetAllRightsAsync(int pageNumber = 1, int pageSize = 10) + { + var query = _unitOfWork.RightRepository.Get(); + + var totalItems = await query.CountAsync(); + + var pagedRights = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (pagedRights, totalItems); + } + + #endregion +} diff --git a/Services/Roles/IRolesService.cs b/Services/Roles/IRolesService.cs new file mode 100755 index 0000000..0a03960 --- /dev/null +++ b/Services/Roles/IRolesService.cs @@ -0,0 +1,14 @@ +using GamificationService.Models.Database; + +namespace GamificationService.Services.Roles; + +public interface IRolesService +{ + Task CreateRoleAsync(string roleName, string description); + Task UpdateRoleAsync(long roleId, string newRoleName, string newDescription); + Task DeleteRoleAsync(long roleId); + Task AddRightToRoleAsync(long roleId, long rightId); + Task RemoveRightFromRoleAsync(long roleId, long rightId); + Task GetRoleByIdAsync(long roleId); + Task<(List Roles, int TotalCount)> GetAllRolesAsync(int pageNumber = 1, int pageSize = 10); +} diff --git a/Services/Roles/RolesService.cs b/Services/Roles/RolesService.cs new file mode 100755 index 0000000..f316f98 --- /dev/null +++ b/Services/Roles/RolesService.cs @@ -0,0 +1,162 @@ +using GamificationService.Database.Repositories; +using GamificationService.Models.Database; +using Microsoft.EntityFrameworkCore; + +namespace GamificationService.Services.Roles; + +public class RolesService : IRolesService +{ + #region Services + + private readonly ILogger _logger; + private readonly UnitOfWork _unitOfWork; + + #endregion + + #region Constructor + + public RolesService(ILogger logger, UnitOfWork unitOfWork) + { + _logger = logger; + _unitOfWork = unitOfWork; + } + + #endregion + + #region Methods + //TODO: refactor database work, to be more beautiful + //ToDo: make better exception handling + public async Task CreateRoleAsync(string roleName, string description) + { + var role = new ApplicationRole(roleName) + { + Description = description + }; + + await _unitOfWork.RoleRepository.InsertAsync(role); + if (await _unitOfWork.SaveAsync()) + { + return role; + } + throw new Exception("Unable to create role"); + } + + public async Task UpdateRoleAsync(long roleId, string newRoleName, string newDescription) + { + var role = await _unitOfWork.RoleRepository.GetByIDAsync(roleId); + + if (role == null) + { + throw new KeyNotFoundException($"Role with ID {roleId} not found"); + } + + role.Name = newRoleName; + role.Description = newDescription; + + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception("Unable to create role"); + } + + return true; + } + + public async Task DeleteRoleAsync(long roleId) + { + var role = await _unitOfWork.RoleRepository.GetByIDAsync(roleId); + + if (role == null) + { + throw new KeyNotFoundException($"Role with ID {roleId} not found"); + } + + _unitOfWork.RoleRepository.Delete(role); + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception("Unable to delete role"); + } + + return true; + } + + public async Task AddRightToRoleAsync(long roleId, long rightId) + { + var role = await _unitOfWork.RoleRepository.Get() + .Include(r => r.RoleRights) + .FirstOrDefaultAsync(r => r.Id == roleId); + + var right = await _unitOfWork.RightRepository.GetByIDAsync(rightId); + + if (role == null || right == null) + { + throw new KeyNotFoundException($"Role or Right not found"); + } + + var existingRight = role.RoleRights.FirstOrDefault(rr => rr.RightId == rightId); + + if (existingRight == null) + { + role.RoleRights.Add(new RoleRight { RoleId = roleId, RightId = rightId }); + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception("Unable to add role right"); + } + } + + return true; + + } + + public async Task RemoveRightFromRoleAsync(long roleId, long rightId) + { + var roleRight = await _unitOfWork.RoleRightRepository.Get() + .FirstOrDefaultAsync(rr => rr.RoleId == roleId && rr.RightId == rightId); + + if (roleRight == null) + { + throw new KeyNotFoundException($"Right not found for role"); + } + + _unitOfWork.RoleRightRepository.Delete(roleRight); + if (!await _unitOfWork.SaveAsync()) + { + throw new Exception("Unable to remove role right"); + } + + return true; + } + + public async Task GetRoleByIdAsync(long roleId) + { + try + { + return await _unitOfWork.RoleRepository.Get() + .Include(r => r.RoleRights) + .ThenInclude(rr => rr.Right) + .FirstOrDefaultAsync(r => r.Id == roleId); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + + } + + public async Task<(List Roles, int TotalCount)> GetAllRolesAsync(int pageNumber = 1, int pageSize = 10) + { + var query = _unitOfWork.RoleRepository.Get() + .Include(r => r.RoleRights) + .ThenInclude(rr => rr.Right); + + var totalItems = await query.CountAsync(); + var pagedRoles = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (pagedRoles, totalItems); + } + #endregion + +} diff --git a/Services/UsersProfile/IUserProfileService.cs b/Services/UsersProfile/IUserProfileService.cs new file mode 100755 index 0000000..cf0fe86 --- /dev/null +++ b/Services/UsersProfile/IUserProfileService.cs @@ -0,0 +1,15 @@ +using GamificationService.Models.Database; +using GamificationService.Models.DTO; + +namespace GamificationService.Services.UsersProfile; + +public interface IUserProfileService +{ + public Task AddUserProfile(long userId, UserProfileCreateDTO userProfile); + public Task AddUserProfile(UserProfile userProfile); + public UserProfile? GetUserProfileByUserId(long id); + public UserProfile? GetUserProfileById(long id); + public Task UpdateUserProfileByUserId(long userId, UserProfileCreateDTO userProfile); + public Task UpdateUserProfile(UserProfile userProfile); + public bool DeleteUserProfile(long id); +} diff --git a/Services/UsersProfile/UserProfileService.cs b/Services/UsersProfile/UserProfileService.cs new file mode 100755 index 0000000..bf87bf2 --- /dev/null +++ b/Services/UsersProfile/UserProfileService.cs @@ -0,0 +1,121 @@ +using AutoMapper; +using GamificationService.Database.Repositories; +using GamificationService.Exceptions.Services.ProfileService; +using GamificationService.Models.Database; +using GamificationService.Models.DTO; + +namespace GamificationService.Services.UsersProfile; + +public class UserProfileService : IUserProfileService +{ +private readonly UnitOfWork _unitOfWork; + + # region Services + + private readonly ILogger _logger; + private readonly IMapper _mapper; + + #endregion + + + #region Constructor + + public UserProfileService(UnitOfWork unitOfWork, ILogger logger, IMapper mapper) + { + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + } + + #endregion + + # region Methods + + public async Task AddUserProfile(long userId, UserProfileCreateDTO userProfile) + { + UserProfile userProfileEntity = _mapper.Map(userProfile); + userProfileEntity.UserId = userId; + return _mapper.Map(await AddUserProfile(userProfileEntity)); + } + + public async Task AddUserProfile(UserProfile userProfile) + { + UserProfile userProfileEntity = userProfile; + + // Make sure a user profile for the given user does not exist yet + if (_unitOfWork.UserProfileRepository.Get(x => x.UserId == userProfile.UserId).Any()) + { + _logger.LogWarning("A user profile already exists for the given user id: {UserId}", userProfile.UserId); + throw new ProfileExistsException($"{userProfile.UserId}"); + } + + await _unitOfWork.UserProfileRepository.InsertAsync(userProfileEntity); + if (await _unitOfWork.SaveAsync()) + { + _logger.LogInformation("User profile added for user id: {UserId}", userProfile.UserId); + return userProfileEntity; + } + + _logger.LogError("Failed to add user profile for user id: {UserId}", userProfile.UserId); + throw new ProfileCreationException(); + } + + public UserProfile? GetUserProfileByUserId(long id) + { + return _unitOfWork.UserProfileRepository.Get(x => x.UserId == id).FirstOrDefault(); + } + + public UserProfile? GetUserProfileById(long id) + { + return _unitOfWork.UserProfileRepository.GetByID(id); + } + + public async Task UpdateUserProfileByUserId(long userId, UserProfileCreateDTO userProfile) + { + var userProfileEntityUpdated = _mapper.Map(userProfile); + var profile = _unitOfWork.UserProfileRepository + .Get(x => x.UserId == userId).FirstOrDefault() ?? throw new ProfileNotFoundException($"{userId}"); + userProfileEntityUpdated.Id = profile.Id; + return await UpdateUserProfile(userProfileEntityUpdated); + } + + public async Task UpdateUserProfile(UserProfile userProfile) + { + var userProfileEntityUpdated = userProfile; + var userProfileEntity = await _unitOfWork.UserProfileRepository.GetByIDAsync(userProfileEntityUpdated.Id); + + if (userProfileEntity == null) + { + throw new ProfileNotFoundException($"{userProfileEntityUpdated.Id}"); + } + + _mapper.Map(userProfileEntityUpdated, userProfileEntity); + + if (!await _unitOfWork.SaveAsync()) + { + throw new ProfileUpdateException($"Failed to update user profile {userProfileEntityUpdated.Id}"); + } + + _logger.LogInformation("User profile updated for user id: {UserId}", userProfile.UserId); + return true; + } + + public bool DeleteUserProfile(long id) + { + var profile = _unitOfWork.UserProfileRepository.GetByID(id); + if (profile == null) + { + throw new ProfileNotFoundException($"{id}"); + } + + _unitOfWork.UserProfileRepository.Delete(id); + if (_unitOfWork.Save()) + { + _logger.LogInformation("User profile deleted: {UserId}", id); + return true; + } + throw new ProfileDeletionException($"Failed to delete user profile {id}"); + } + + #endregion +} diff --git a/Utils/Clients/EmailClient.cs b/Utils/Clients/EmailClient.cs new file mode 100755 index 0000000..1142046 --- /dev/null +++ b/Utils/Clients/EmailClient.cs @@ -0,0 +1,38 @@ +using System.Net.Mail; +using GamificationService.Exceptions.UtilServices.Email; + +namespace GamificationService.Utils; + +public class EmailClient(SmtpClient smtpClient, string emailFrom, ILogger logger) +{ + #region Fields + + private readonly string _emailFrom = emailFrom; + private readonly ILogger _logger = logger; + + #endregion + + #region Methods + + /// + /// Sends the email using the SmtpClient instance. + /// + /// Email to send. + /// If the SmtpClient instance fails to send the email. + /// Task that represents the asynchronous operation. + public async Task SendEmail(MailMessage email, string emailTo) + { + try + { + email.To.Add(new MailAddress(emailTo)); + await smtpClient.SendMailAsync(email); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + throw new SendEmailException("Failed to send email", ex); + } + } + + #endregion +} diff --git a/Utils/Clients/PushNotificationsClient.cs b/Utils/Clients/PushNotificationsClient.cs new file mode 100755 index 0000000..855ab18 --- /dev/null +++ b/Utils/Clients/PushNotificationsClient.cs @@ -0,0 +1,150 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using GamificationService.Exceptions.UtilServices.Api; + +namespace GamificationService.Utils; + +public class PushNotificationsClient +{ + #region Fields + + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _applicationToken; + private readonly string _projectId; + #endregion + + #region Constructor + + public PushNotificationsClient(HttpClient httpClient, ILogger logger, string applicationToken, string projectId) + { + _httpClient = httpClient; + _logger = logger; + _applicationToken = applicationToken; + _projectId = projectId; + } + + #endregion + + #region Methods + + public async Task SendPushNotification(PushNotification pushNotification) + { + try + { + var payload = new + { + message = new + { + token = _applicationToken, + notification = new + { + body = pushNotification.Message, + title = pushNotification.Title, + image = pushNotification.Image + }, + android = new + { + notification = new + { + body = pushNotification.Message, + title = pushNotification.Title, + image = pushNotification.Image, + click_action = pushNotification.ClickAction, + click_action_type = pushNotification.ClickActionType + } + } + } + }; + + var jsonPayload = JsonSerializer.Serialize(payload); + + var request = new HttpRequestMessage(HttpMethod.Post,$"/{_projectId}/messages") + { + Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + }; + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification. Status Code: {response.StatusCode}, Response: {responseContent}"); + throw new BadRequestException($"Failed to send push notification: {response.StatusCode}"); + } + else if (response.StatusCode == HttpStatusCode.Forbidden) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification: {response.StatusCode}, Response: {responseContent}"); + throw new ForbiddenException($"Failed to send push notification: {response.StatusCode}"); + } + else + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification: {response.StatusCode}, Response: {responseContent}"); + throw new Exception($"Failed to send push notification: {response.StatusCode}"); + } + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + } + + public async Task SendPushNotification(PushNotification pushNotification, string topic) + { + try + { + var payload = new + { + message = new + { + notification = new + { + body = pushNotification.Message, + title = pushNotification.Title, + image = pushNotification.Image + } + } + }; + + var jsonPayload = JsonSerializer.Serialize(payload); + + var request = new HttpRequestMessage(HttpMethod.Post,$"/{_projectId}/topics/{topic}/publish") + { + Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + }; + + var response = await _httpClient.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification. Status Code: {response.StatusCode}, Response: {responseContent}"); + throw new BadRequestException($"Failed to send push notification: {response.StatusCode}"); + } + else if (response.StatusCode == HttpStatusCode.Forbidden) + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification: {response.StatusCode}, Response: {responseContent}"); + throw new ForbiddenException($"Failed to send push notification: {response.StatusCode}"); + } + else + { + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Failed to send push notification: {response.StatusCode}, Response: {responseContent}"); + throw new Exception($"Failed to send push notification: {response.StatusCode}"); + } + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + } + + #endregion +} diff --git a/Utils/Enums/ClickActionType.cs b/Utils/Enums/ClickActionType.cs new file mode 100755 index 0000000..f462829 --- /dev/null +++ b/Utils/Enums/ClickActionType.cs @@ -0,0 +1,6 @@ +namespace GamificationService.Utils; + +public enum ClickActionType +{ + NONE +} diff --git a/Utils/Enums/Gender.cs b/Utils/Enums/Gender.cs new file mode 100644 index 0000000..00e18d2 --- /dev/null +++ b/Utils/Enums/Gender.cs @@ -0,0 +1,7 @@ +namespace GamificationService.Utils.Enums; + +public enum Gender +{ + Male, + Female +} diff --git a/Utils/Enums/InstructionTestScoreCalcMethod.cs b/Utils/Enums/InstructionTestScoreCalcMethod.cs new file mode 100644 index 0000000..51184d9 --- /dev/null +++ b/Utils/Enums/InstructionTestScoreCalcMethod.cs @@ -0,0 +1,8 @@ +namespace GamificationService.Utils.Enums +{ + public enum InstructionTestScoreCalcMethod + { + AverageGrade, + MaxGrade + } +} diff --git a/Utils/Enums/NotificationInformationType.cs b/Utils/Enums/NotificationInformationType.cs new file mode 100755 index 0000000..0c78444 --- /dev/null +++ b/Utils/Enums/NotificationInformationType.cs @@ -0,0 +1,9 @@ +namespace GamificationService.Utils; + +public enum NotificationInformationType +{ + AUTH, + INFO, + WARNING, + ERROR +} diff --git a/Utils/Factory/MailNotificationsFactory.cs b/Utils/Factory/MailNotificationsFactory.cs new file mode 100755 index 0000000..e123071 --- /dev/null +++ b/Utils/Factory/MailNotificationsFactory.cs @@ -0,0 +1,21 @@ +using System.Net.Mail; + +namespace GamificationService.Utils.Factory; + +public class MailNotificationsFactory +{ + public static Notification CreateNotification(NotificationInformationType type, + string title, + string message,List attachments) + { + return new MailNotification(type, title, message, attachments); + } + public Notification CreateNotification(NotificationInformationType type, + string title, + string message) + { + + return new MailNotification(type, title, message); + + } +} diff --git a/Utils/Factory/PushNotificationsFactory.cs b/Utils/Factory/PushNotificationsFactory.cs new file mode 100755 index 0000000..5290e58 --- /dev/null +++ b/Utils/Factory/PushNotificationsFactory.cs @@ -0,0 +1,28 @@ +namespace GamificationService.Utils.Factory; + +public class PushNotificationsFactory +{ + public Notification CreateNotification(NotificationInformationType type, + string title, + string message) + { + return new PushNotification(type, title, message); + } + + public Notification CreateNotification(NotificationInformationType type, + string title, + string message, + string image) + { + return new PushNotification(type, title, message, image); + } + public Notification CreateNotification(NotificationInformationType type, + string title, + string message, + string? image, + string clickAction, + ClickActionType clickActionType) + { + return new PushNotification(type, title, message, image, clickAction, clickActionType); + } +} diff --git a/Utils/MailNotification.cs b/Utils/MailNotification.cs new file mode 100755 index 0000000..a035fa2 --- /dev/null +++ b/Utils/MailNotification.cs @@ -0,0 +1,111 @@ +using System.Net.Mail; + +namespace GamificationService.Utils; + +public class MailNotification : Notification +{ + #region Fields + + private List _attachments; + + #endregion + + #region Properties + public List Attachments { get => _attachments; } + + #endregion + + #region Constructor + + public MailNotification(NotificationInformationType type, string title, string message, List attachments) : base(type, title, message) + { + _attachments = attachments; + } + public MailNotification(NotificationInformationType type, string title, string message) : base(type, title, message) + { + } + #endregion + + #region Methods + + public MailMessage ConvertToMailMessage() + { + var mailMessage = new MailMessage + { + Subject = CreateTitle(), + Body = CreateBody(), + IsBodyHtml = true, + Priority = GetPriority() + }; + if (_attachments != null) + { + mailMessage.Attachments.ToList().AddRange(_attachments); + } + return mailMessage; + } + + #endregion + + #region Private Methods + private string CreateTitle() + { + switch (Type) + { + case NotificationInformationType.AUTH: + return "Авторизация " + Title; + case NotificationInformationType.INFO: + return "Информация "+ Title; + case NotificationInformationType.WARNING: + return "Предупреждение "+ Title; + case NotificationInformationType.ERROR: + return "Ошибка "+ Title; + default: + return "Информация "+ Title; + } + } + + private string CreateBody() + { + string formattedMessage; + + switch (Type) + { + case NotificationInformationType.AUTH: + formattedMessage = "Вы успешно авторизовались."; + break; + case NotificationInformationType.INFO: + formattedMessage = "Это информационное сообщение."; + break; + case NotificationInformationType.WARNING: + formattedMessage = "Внимание! Обратите внимание на это предупреждение."; + break; + case NotificationInformationType.ERROR: + formattedMessage = "Произошла ошибка. Пожалуйста, проверьте детали."; + break; + default: + formattedMessage = "Сообщение не определено."; + break; + } + + return $"

{formattedMessage} {Message}

"; + } + + private MailPriority GetPriority() + { + switch (Type) + { + case NotificationInformationType.AUTH: + return MailPriority.High; + case NotificationInformationType.INFO: + return MailPriority.Normal; + case NotificationInformationType.WARNING: + return MailPriority.Low; + case NotificationInformationType.ERROR: + return MailPriority.High; + default: + return MailPriority.Normal; + } + } + + #endregion +} diff --git a/Utils/Notification.cs b/Utils/Notification.cs new file mode 100755 index 0000000..0d8e099 --- /dev/null +++ b/Utils/Notification.cs @@ -0,0 +1,33 @@ +using System.Net.Mail; + +namespace GamificationService.Utils; + +public abstract class Notification +{ + #region Fields + + private string _title; + private string _message; + private NotificationInformationType _type; + + #endregion + + #region Parameters + + public string Title { get => _title;} + public string Message { get => _message; } + public NotificationInformationType Type { get => _type; } + + #endregion + + #region Constructor + + public Notification(NotificationInformationType type, string title, string message) + { + _type = type; + _title = title; + _message = message; + } + + #endregion +} diff --git a/Utils/PushNotification.cs b/Utils/PushNotification.cs new file mode 100755 index 0000000..034212b --- /dev/null +++ b/Utils/PushNotification.cs @@ -0,0 +1,40 @@ +namespace GamificationService.Utils; + +public class PushNotification : Notification +{ + #region Fields + + private readonly string? _image; + private readonly string? _clickAction; + private readonly ClickActionType? _clickActionType; + + #endregion + + #region Properties + + public string? Image { get => _image; } + public string? ClickAction { get => _clickAction; } + public int? ClickActionType { get => (int)_clickActionType; } + + #endregion + + #region Constructor + + public PushNotification(NotificationInformationType type, string title, string message, string image, string clickAction, ClickActionType clickActionType) : base(type, title, message) + { + _image = image; + _clickAction = clickAction; + _clickActionType = clickActionType; + } + + public PushNotification(NotificationInformationType type, string title, string message, string? image) : base(type, title, message) + { + _image = image; + } + + public PushNotification(NotificationInformationType type, string title, string message) : base(type, title, message) + { + } + + #endregion +} diff --git a/Utils/TwoFactorProvider.cs b/Utils/TwoFactorProvider.cs new file mode 100644 index 0000000..0184174 --- /dev/null +++ b/Utils/TwoFactorProvider.cs @@ -0,0 +1,10 @@ +namespace GamificationService.Utils; + +public enum TwoFactorProvider +{ + NONE, + EMAIL, + PHONE, + PUSH, + AUTHENTICATOR +} diff --git a/appsettings.Development.template.json b/appsettings.Development.template.json new file mode 100755 index 0000000..8106b9f --- /dev/null +++ b/appsettings.Development.template.json @@ -0,0 +1,33 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "JwtSettings": { + "SecretKey": "5bf26600dc03db43640b080b32f496ee72e34ef43dcc4f676707d91be12bf64ccbc8ba28a4a43a227f945fbef3a6c27dea8ac22d5bfed847901ca60178bc9dfaa4437a5d726a2f837090449de205e208d9cc49e156eb661c7896275f4f8cf902afca9f317e64f406c93bc78c06b7dca25fd429524755e732b758c5b1ea4fac59a1c93076cd5acafc4bbc0ece89f9422a4ebaf186554a443bc6d4b1baec605897eec20622fa647abde9b8522e04fac4d84d050c8dac4aa10503bbfb6f147376ba32f77d38926792b363d2c45b87a13af0f5db30c525768567adc5eb2b8ea2d77e56b381d4e93db5a167f0b3b8508ff3765c32d2dac969e058bd7f7e94c74e3d0f", + "Issuer": "adminPanel", + "Audience": "adminPanel", + "TokenLifetime": 30 + }, + "Redis": { + "ConnectionString": "your_redis_connection_string", + "InstanceName": "your_redis_instance_name" + }, + "DatabaseSettings": { + "Hostname": "your-hostname", + "Port": "your-port", + "Name": "your-database-name", + "Username": "your-username", + "Password": "your-password" + }, + "EmailSettings": { + "Host": "smtp.gmail.com", + "Port": 587, + "Username": "email", + "Password": "password", + "EmailFrom": "email@mail.ru" + } +} diff --git a/appsettings.template.json b/appsettings.template.json new file mode 100755 index 0000000..7d6b398 --- /dev/null +++ b/appsettings.template.json @@ -0,0 +1,32 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "JwtSettings": { + "SecretKey": "5bf26600dc03db43640b080b32f496ee72e34ef43dcc4f676707d91be12bf64ccbc8ba28a4a43a227f945fbef3a6c27dea8ac22d5bfed847901ca60178bc9dfaa4437a5d726a2f837090449de205e208d9cc49e156eb661c7896275f4f8cf902afca9f317e64f406c93bc78c06b7dca25fd429524755e732b758c5b1ea4fac59a1c93076cd5acafc4bbc0ece89f9422a4ebaf186554a443bc6d4b1baec605897eec20622fa647abde9b8522e04fac4d84d050c8dac4aa10503bbfb6f147376ba32f77d38926792b363d2c45b87a13af0f5db30c525768567adc5eb2b8ea2d77e56b381d4e93db5a167f0b3b8508ff3765c32d2dac969e058bd7f7e94c74e3d0f", + "Issuer": "adminPanel", + "Audience": "adminPanel", + "TokenLifetime": 30 + }, + "Redis": { + "ConnectionString": "your_redis_connection_string", + "InstanceName": "your_redis_instance_name" + }, + "DatabaseSettings": { + "Hostname": "your-hostname", + "Port": "your-port", + "Name": "your-database-name", + "Username": "your-username", + "Password": "your-password" + }, + "NotificationSettings": { + "ApiKey": "", + "Token": "", + "Url": "", + "ProjectId": "" + } +}