From 40342a0e141bfc494093af3511318db804bf040b Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 1 Oct 2025 01:56:05 +0300 Subject: [PATCH] something --- .gitignore | 1324 ++++++------ LCT2025.sln | 32 +- LCT2025.sln.DotSettings.user | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 82 +- .../Middleware/ErrorHandlingMiddleware.cs | 92 +- LctMonolith/Application/Options/JwtOptions.cs | 28 +- .../Application/Options/S3StorageOptions.cs | 24 +- .../Controllers/AnalyticsController.cs | 58 +- LctMonolith/Controllers/AuthController.cs | 172 +- LctMonolith/Controllers/DialogueController.cs | 150 +- .../Controllers/InventoryController.cs | 68 +- .../MissionCategoriesController.cs | 116 +- LctMonolith/Controllers/MissionsController.cs | 224 +- .../Controllers/NotificationsController.cs | 116 +- LctMonolith/Controllers/PlayersController.cs | 156 +- LctMonolith/Controllers/ProfileController.cs | 134 +- LctMonolith/Controllers/RanksController.cs | 144 +- LctMonolith/Controllers/RewardController.cs | 130 +- LctMonolith/Controllers/SkillsController.cs | 158 +- LctMonolith/Controllers/StoreController.cs | 86 +- LctMonolith/Database/Data/AppDbContext.cs | 448 ++-- LctMonolith/Database/Data/DbSeeder.cs | 94 +- LctMonolith/Database/Models/EventType.cs | 38 +- .../Repositories/GenericRepository.cs | 112 +- .../Repositories/IGenericRepository.cs | 50 +- .../Database/UnitOfWork/IUnitOfWork.cs | 80 +- LctMonolith/Database/UnitOfWork/UnitOfWork.cs | 220 +- LctMonolith/Dockerfile | 46 +- LctMonolith/LctMonolith.csproj | 86 +- LctMonolith/LctMonolith.http | 12 +- .../Models/DTO/CreateMissionCategoryDto.cs | 6 +- LctMonolith/Models/DTO/CreateMissionDto.cs | 24 +- LctMonolith/Models/DTO/CreateRankDto.cs | 6 +- LctMonolith/Models/DTO/CreateSkillDto.cs | 6 +- .../Models/DTO/MissionCompletionResult.cs | 22 +- .../Models/DTO/MissionValidationResult.cs | 16 +- LctMonolith/Models/DTO/PlayerProgress.cs | 30 +- LctMonolith/Models/DTO/SkillProgress.cs | 18 +- LctMonolith/Models/Database/AppUser.cs | 46 +- .../Models/Database/AuditableEntity.cs | 18 +- LctMonolith/Models/Database/Dialogue.cs | 32 +- .../Models/Database/DialogueMessage.cs | 54 +- .../Database/DialogueMessageResponseOption.cs | 32 +- LctMonolith/Models/Database/EventLog.cs | 22 +- LctMonolith/Models/Database/Mission.cs | 46 +- .../Models/Database/MissionCategory.cs | 18 +- .../Models/Database/MissionItemReward.cs | 18 +- .../Models/Database/MissionRankRule.cs | 20 +- .../Models/Database/MissionSkillReward.cs | 22 +- LctMonolith/Models/Database/Notification.cs | 28 +- LctMonolith/Models/Database/Player.cs | 28 +- LctMonolith/Models/Database/PlayerMission.cs | 28 +- LctMonolith/Models/Database/PlayerSkill.cs | 22 +- LctMonolith/Models/Database/Profile.cs | 42 +- LctMonolith/Models/Database/Rank.cs | 26 +- .../Models/Database/RankMissionRule.cs | 20 +- LctMonolith/Models/Database/RankSkillRule.cs | 22 +- LctMonolith/Models/Database/RefreshToken.cs | 24 +- LctMonolith/Models/Database/Skill.cs | 20 +- LctMonolith/Models/Database/StoreItem.cs | 24 +- LctMonolith/Models/Database/Transaction.cs | 26 +- .../Models/Database/TransactionType.cs | 6 +- .../Models/Database/UserInventoryItem.cs | 24 +- LctMonolith/Models/Enums/Character.cs | 22 +- .../Models/Enums/CharacterAnimation.cs | 36 +- LctMonolith/Models/Enums/MessageStyle.cs | 18 +- LctMonolith/Program.cs | 268 +-- LctMonolith/Properties/launchSettings.json | 46 +- LctMonolith/Services/AnalyticsService.cs | 82 +- .../Services/Contracts/IAnalyticsService.cs | 16 +- .../Services/Contracts/IDialogueService.cs | 24 +- .../Services/Contracts/IFileStorageService.cs | 16 +- .../Services/Contracts/IInventoryService.cs | 16 +- .../Contracts/IMissionCategoryService.cs | 28 +- .../Services/Contracts/IMissionService.cs | 34 +- .../Contracts/INotificationService.cs | 24 +- .../Services/Contracts/IPlayerService.cs | 28 +- .../Services/Contracts/IProfileService.cs | 22 +- .../Contracts/IProgressTrackingService.cs | 28 +- .../Services/Contracts/IRankService.cs | 30 +- .../Services/Contracts/IRewardService.cs | 22 +- .../Contracts/IRuleValidationService.cs | 20 +- .../Services/Contracts/ISkillService.cs | 32 +- .../Services/Contracts/IStoreService.cs | 18 +- .../Services/Contracts/ITokenService.cs | 22 +- LctMonolith/Services/DialogueService.cs | 110 +- LctMonolith/Services/InventoryService.cs | 52 +- .../Services/MissionCategoryService.cs | 98 +- LctMonolith/Services/MissionService.cs | 330 +-- .../Services/Models/AnalyticsSummary.cs | 22 +- LctMonolith/Services/Models/AuthRequest.cs | 18 +- .../Services/Models/CompetencyRewardModel.cs | 16 +- .../Services/Models/CreateMissionModel.cs | 30 +- .../Services/Models/ProgressSnapshot.cs | 46 +- .../Services/Models/PurchaseRequest.cs | 14 +- LctMonolith/Services/Models/RefreshRequest.cs | 12 +- LctMonolith/Services/Models/RevokeRequest.cs | 12 +- LctMonolith/Services/Models/TokenPair.cs | 6 +- LctMonolith/Services/NotificationService.cs | 136 +- LctMonolith/Services/PlayerService.cs | 306 +-- LctMonolith/Services/ProfileService.cs | 242 +-- .../Services/ProgressTrackingService.cs | 206 +- LctMonolith/Services/RankService.cs | 150 +- LctMonolith/Services/RewardService.cs | 184 +- LctMonolith/Services/RuleValidationService.cs | 128 +- LctMonolith/Services/S3FileStorageService.cs | 204 +- LctMonolith/Services/SkillService.cs | 118 +- LctMonolith/Services/StoreService.cs | 124 +- LctMonolith/Services/TokenService.cs | 246 +-- LctMonolith/appsettings.Development.json | 70 +- LctMonolith/appsettings.json | 72 +- LctMonolith/openapi-gamification.yaml | 1854 ++++++++--------- 112 files changed, 5468 insertions(+), 5468 deletions(-) diff --git a/.gitignore b/.gitignore index 8403eba..74ef00c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,662 +1,662 @@ -# Created by https://www.toptal.com/developers/gitignore/api/vim,rider,visualstudiocode,git,aspnetcore,csharp,dotnetcore -# Edit at https://www.toptal.com/developers/gitignore?templates=vim,rider,visualstudiocode,git,aspnetcore,csharp,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/ - -### Csharp ### -## -## 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 -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider - -### DotnetCore ### -# .NET Core build folders -bin/ -obj/ - -# Common node modules locations -/node_modules -/wwwroot/node_modules - -### Git ### -# Created by git for backups. To disable backups in Git: -# $ git config --global mergetool.keepBackup false -*.orig - -# Created by git when using merge tools for conflicts -*.BACKUP.* -*.BASE.* -*.LOCAL.* -*.REMOTE.* -*_BACKUP_*.txt -*_BASE_*.txt -*_LOCAL_*.txt -*_REMOTE_*.txt - -### 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/*.code-snippets - -# Local History for Visual Studio Code - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# End of https://www.toptal.com/developers/gitignore/api/vim,rider,visualstudiocode,git,aspnetcore,csharp,dotnetcore +# Created by https://www.toptal.com/developers/gitignore/api/vim,rider,visualstudiocode,git,aspnetcore,csharp,dotnetcore +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,rider,visualstudiocode,git,aspnetcore,csharp,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/ + +### Csharp ### +## +## 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 +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider + +### DotnetCore ### +# .NET Core build folders +bin/ +obj/ + +# Common node modules locations +/node_modules +/wwwroot/node_modules + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### 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/*.code-snippets + +# Local History for Visual Studio Code + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/vim,rider,visualstudiocode,git,aspnetcore,csharp,dotnetcore diff --git a/LCT2025.sln b/LCT2025.sln index ee320c6..e07ef95 100644 --- a/LCT2025.sln +++ b/LCT2025.sln @@ -1,16 +1,16 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LctMonolith", "LctMonolith\LctMonolith.csproj", "{EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LctMonolith", "LctMonolith\LctMonolith.csproj", "{EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA37C010-2A5C-4B2E-A6B9-1E7EBB39D497}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/LCT2025.sln.DotSettings.user b/LCT2025.sln.DotSettings.user index 6652580..a572cea 100644 --- a/LCT2025.sln.DotSettings.user +++ b/LCT2025.sln.DotSettings.user @@ -1,2 +1,2 @@ - + ForceIncluded \ No newline at end of file diff --git a/LctMonolith/Application/Extensions/ServiceCollectionExtensions.cs b/LctMonolith/Application/Extensions/ServiceCollectionExtensions.cs index 6a1e9c7..08b5d09 100644 --- a/LctMonolith/Application/Extensions/ServiceCollectionExtensions.cs +++ b/LctMonolith/Application/Extensions/ServiceCollectionExtensions.cs @@ -1,41 +1,41 @@ -using LctMonolith.Application.Options; -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Services; -using LctMonolith.Services.Contracts; -using LctMonolith.Services.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace LctMonolith.Application.Extensions; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration) - { - // Unit of Work - services.AddScoped(); - - // Core domain / gamification services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.Configure(configuration.GetSection("S3")); - services.AddSingleton(); - services.AddScoped(); - - return services; - } -} +using LctMonolith.Application.Options; +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Services; +using LctMonolith.Services.Contracts; +using LctMonolith.Services.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace LctMonolith.Application.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration) + { + // Unit of Work + services.AddScoped(); + + // Core domain / gamification services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.Configure(configuration.GetSection("S3")); + services.AddSingleton(); + services.AddScoped(); + + return services; + } +} diff --git a/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs b/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs index fa30be0..b26789c 100644 --- a/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs +++ b/LctMonolith/Application/Middleware/ErrorHandlingMiddleware.cs @@ -1,46 +1,46 @@ -using System.Net; -using System.Text.Json; -using Serilog; - -namespace LctMonolith.Application.Middleware; - -/// -/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response. -/// -public class ErrorHandlingMiddleware -{ - private readonly RequestDelegate _next; - public ErrorHandlingMiddleware(RequestDelegate next) => _next = next; - - public async Task Invoke(HttpContext ctx) - { - try - { - await _next(ctx); - } - catch (OperationCanceledException) - { - // Client aborted request (non-standard 499 code used by some proxies) - if (!ctx.Response.HasStarted) - { - ctx.Response.StatusCode = 499; // Client Closed Request (custom) - } - } - catch (Exception ex) - { - Log.Error(ex, "Unhandled exception"); - if (ctx.Response.HasStarted) throw; - - ctx.Response.ContentType = "application/json"; - ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } }; - await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload)); - } - } -} - -public static class ErrorHandlingMiddlewareExtensions -{ - /// Adds global error handling middleware. - public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware(); -} +using System.Net; +using System.Text.Json; +using Serilog; + +namespace LctMonolith.Application.Middleware; + +/// +/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response. +/// +public class ErrorHandlingMiddleware +{ + private readonly RequestDelegate _next; + public ErrorHandlingMiddleware(RequestDelegate next) => _next = next; + + public async Task Invoke(HttpContext ctx) + { + try + { + await _next(ctx); + } + catch (OperationCanceledException) + { + // Client aborted request (non-standard 499 code used by some proxies) + if (!ctx.Response.HasStarted) + { + ctx.Response.StatusCode = 499; // Client Closed Request (custom) + } + } + catch (Exception ex) + { + Log.Error(ex, "Unhandled exception"); + if (ctx.Response.HasStarted) throw; + + ctx.Response.ContentType = "application/json"; + ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } }; + await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload)); + } + } +} + +public static class ErrorHandlingMiddlewareExtensions +{ + /// Adds global error handling middleware. + public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware(); +} diff --git a/LctMonolith/Application/Options/JwtOptions.cs b/LctMonolith/Application/Options/JwtOptions.cs index a44b98c..1d2a544 100644 --- a/LctMonolith/Application/Options/JwtOptions.cs +++ b/LctMonolith/Application/Options/JwtOptions.cs @@ -1,14 +1,14 @@ -namespace LctMonolith.Application.Options; - -/// -/// JWT issuing configuration loaded from appsettings (section Jwt). -/// -public class JwtOptions -{ - public string Key { get; set; } = string.Empty; - public string Issuer { get; set; } = string.Empty; - public string Audience { get; set; } = string.Empty; - public int AccessTokenMinutes { get; set; } = 60; - public int RefreshTokenDays { get; set; } = 7; -} - +namespace LctMonolith.Application.Options; + +/// +/// JWT issuing configuration loaded from appsettings (section Jwt). +/// +public class JwtOptions +{ + public string Key { get; set; } = string.Empty; + public string Issuer { get; set; } = string.Empty; + public string Audience { get; set; } = string.Empty; + public int AccessTokenMinutes { get; set; } = 60; + public int RefreshTokenDays { get; set; } = 7; +} + diff --git a/LctMonolith/Application/Options/S3StorageOptions.cs b/LctMonolith/Application/Options/S3StorageOptions.cs index e0dcf09..6efa11e 100644 --- a/LctMonolith/Application/Options/S3StorageOptions.cs +++ b/LctMonolith/Application/Options/S3StorageOptions.cs @@ -1,12 +1,12 @@ -namespace LctMonolith.Application.Options; - -public class S3StorageOptions -{ - public string Endpoint { get; set; } = string.Empty; - public bool UseSsl { get; set; } = true; - public string AccessKey { get; set; } = string.Empty; - public string SecretKey { get; set; } = string.Empty; - public string Bucket { get; set; } = "avatars"; - public string? PublicBaseUrl { get; set; } // optional CDN / reverse proxy base - public int PresignExpirationMinutes { get; set; } = 60; -} +namespace LctMonolith.Application.Options; + +public class S3StorageOptions +{ + public string Endpoint { get; set; } = string.Empty; + public bool UseSsl { get; set; } = true; + public string AccessKey { get; set; } = string.Empty; + public string SecretKey { get; set; } = string.Empty; + public string Bucket { get; set; } = "avatars"; + public string? PublicBaseUrl { get; set; } // optional CDN / reverse proxy base + public int PresignExpirationMinutes { get; set; } = 60; +} diff --git a/LctMonolith/Controllers/AnalyticsController.cs b/LctMonolith/Controllers/AnalyticsController.cs index 5e89f40..fc978a3 100644 --- a/LctMonolith/Controllers/AnalyticsController.cs +++ b/LctMonolith/Controllers/AnalyticsController.cs @@ -1,29 +1,29 @@ -using LctMonolith.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -/// -/// Basic analytics endpoints. -/// -[ApiController] -[Route("api/analytics")] -[Authorize] -public class AnalyticsController : ControllerBase -{ - private readonly IAnalyticsService _analytics; - public AnalyticsController(IAnalyticsService analytics) - { - _analytics = analytics; - } - - /// Get aggregate system summary metrics. - [HttpGet("summary")] - public async Task GetSummary(CancellationToken ct) - { - var summary = await _analytics.GetSummaryAsync(ct); - return Ok(summary); - } -} - +using LctMonolith.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +/// +/// Basic analytics endpoints. +/// +[ApiController] +[Route("api/analytics")] +[Authorize] +public class AnalyticsController : ControllerBase +{ + private readonly IAnalyticsService _analytics; + public AnalyticsController(IAnalyticsService analytics) + { + _analytics = analytics; + } + + /// Get aggregate system summary metrics. + [HttpGet("summary")] + public async Task GetSummary(CancellationToken ct) + { + var summary = await _analytics.GetSummaryAsync(ct); + return Ok(summary); + } +} + diff --git a/LctMonolith/Controllers/AuthController.cs b/LctMonolith/Controllers/AuthController.cs index f785624..e6be810 100644 --- a/LctMonolith/Controllers/AuthController.cs +++ b/LctMonolith/Controllers/AuthController.cs @@ -1,86 +1,86 @@ -using System.Security.Claims; -using LctMonolith.Models.Database; -using LctMonolith.Services; -using LctMonolith.Services.Contracts; -using LctMonolith.Services.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.Data; -using Microsoft.AspNetCore.Mvc; -using RefreshRequest = LctMonolith.Services.Models.RefreshRequest; - -namespace LctMonolith.Controllers; - -/// -/// Authentication endpoints (mocked local identity + JWT issuing). -/// -[ApiController] -[Route("api/auth")] -public class AuthController : ControllerBase -{ - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly ITokenService _tokenService; - - public AuthController(UserManager userManager, SignInManager signInManager, ITokenService tokenService) - { - _userManager = userManager; - _signInManager = signInManager; - _tokenService = tokenService; - } - - /// Registers a new user (simplified). - [HttpPost("register")] - [AllowAnonymous] - public async Task> Register(AuthRequest req, CancellationToken ct) - { - var existing = await _userManager.FindByEmailAsync(req.Email); - if (existing != null) return Conflict("Email already registered"); - var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName }; - var result = await _userManager.CreateAsync(user, req.Password); - if (!result.Succeeded) return BadRequest(result.Errors); - var tokens = await _tokenService.IssueAsync(user, ct); - return Ok(tokens); - } - - /// Login with email + password. - [HttpPost("login")] - [AllowAnonymous] - public async Task> Login(AuthRequest req, CancellationToken ct) - { - var user = await _userManager.FindByEmailAsync(req.Email); - if (user == null) return Unauthorized(); - var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, lockoutOnFailure: false); - if (!passOk.Succeeded) return Unauthorized(); - var tokens = await _tokenService.IssueAsync(user, ct); - return Ok(tokens); - } - - /// Refresh access token by refresh token. - [HttpPost("refresh")] - [AllowAnonymous] - public async Task> Refresh(RefreshRequest req, CancellationToken ct) - { - var pair = await _tokenService.RefreshAsync(req.RefreshToken, ct); - return Ok(pair); - } - - /// Revoke refresh token (logout). - [HttpPost("revoke")] - [Authorize] - public async Task Revoke(RevokeRequest req, CancellationToken ct) - { - await _tokenService.RevokeAsync(req.RefreshToken, ct); - return NoContent(); - } - - /// Returns current user id (debug). - [HttpGet("me")] - [Authorize] - public ActionResult Me() - { - var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name); - return Ok(new { userId = id }); - } -} - +using System.Security.Claims; +using LctMonolith.Models.Database; +using LctMonolith.Services; +using LctMonolith.Services.Contracts; +using LctMonolith.Services.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.Data; +using Microsoft.AspNetCore.Mvc; +using RefreshRequest = LctMonolith.Services.Models.RefreshRequest; + +namespace LctMonolith.Controllers; + +/// +/// Authentication endpoints (mocked local identity + JWT issuing). +/// +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ITokenService _tokenService; + + public AuthController(UserManager userManager, SignInManager signInManager, ITokenService tokenService) + { + _userManager = userManager; + _signInManager = signInManager; + _tokenService = tokenService; + } + + /// Registers a new user (simplified). + [HttpPost("register")] + [AllowAnonymous] + public async Task> Register(AuthRequest req, CancellationToken ct) + { + var existing = await _userManager.FindByEmailAsync(req.Email); + if (existing != null) return Conflict("Email already registered"); + var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName }; + var result = await _userManager.CreateAsync(user, req.Password); + if (!result.Succeeded) return BadRequest(result.Errors); + var tokens = await _tokenService.IssueAsync(user, ct); + return Ok(tokens); + } + + /// Login with email + password. + [HttpPost("login")] + [AllowAnonymous] + public async Task> Login(AuthRequest req, CancellationToken ct) + { + var user = await _userManager.FindByEmailAsync(req.Email); + if (user == null) return Unauthorized(); + var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, lockoutOnFailure: false); + if (!passOk.Succeeded) return Unauthorized(); + var tokens = await _tokenService.IssueAsync(user, ct); + return Ok(tokens); + } + + /// Refresh access token by refresh token. + [HttpPost("refresh")] + [AllowAnonymous] + public async Task> Refresh(RefreshRequest req, CancellationToken ct) + { + var pair = await _tokenService.RefreshAsync(req.RefreshToken, ct); + return Ok(pair); + } + + /// Revoke refresh token (logout). + [HttpPost("revoke")] + [Authorize] + public async Task Revoke(RevokeRequest req, CancellationToken ct) + { + await _tokenService.RevokeAsync(req.RefreshToken, ct); + return NoContent(); + } + + /// Returns current user id (debug). + [HttpGet("me")] + [Authorize] + public ActionResult Me() + { + var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name); + return Ok(new { userId = id }); + } +} + diff --git a/LctMonolith/Controllers/DialogueController.cs b/LctMonolith/Controllers/DialogueController.cs index 264666b..1dc990f 100644 --- a/LctMonolith/Controllers/DialogueController.cs +++ b/LctMonolith/Controllers/DialogueController.cs @@ -1,75 +1,75 @@ -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/dialogue")] -[Authorize] -public class DialogueController : ControllerBase -{ - private readonly IDialogueService _dialogueService; - - public DialogueController(IDialogueService dialogueService) - { - _dialogueService = dialogueService; - } - - [HttpGet("mission/{missionId:guid}")] - public async Task GetByMission(Guid missionId) - { - var d = await _dialogueService.GetDialogueByMissionIdAsync(missionId); - return d == null ? NotFound() : Ok(d); - } - - [HttpGet("message/{messageId:guid}")] - public async Task GetMessage(Guid messageId) - { - var m = await _dialogueService.GetDialogueMessageByIdAsync(messageId); - return m == null ? NotFound() : Ok(m); - } - - [HttpGet("message/{messageId:guid}/options")] - public async Task GetOptions(Guid messageId) - { - var opts = await _dialogueService.GetResponseOptionsAsync(messageId); - return Ok(opts); - } - - public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId); - - [HttpPost("message/{messageId:guid}/respond")] - public async Task Respond(Guid messageId, DialogueResponseRequest req) - { - var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId); - if (next == null) return Ok(new { end = true }); - return Ok(next); - } - - public class CreateDialogueRequest - { - public Guid MissionId { get; set; } - public Guid InitialDialogueMessageId { get; set; } - public Guid InterimDialogueMessageId { get; set; } - public Guid EndDialogueMessageId { get; set; } - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreateDialogueRequest dto) - { - var d = new Dialogue - { - Id = Guid.NewGuid(), - MissionId = dto.MissionId, - InitialDialogueMessageId = dto.InitialDialogueMessageId, - InterimDialogueMessageId = dto.InterimDialogueMessageId, - EndDialogueMessageId = dto.EndDialogueMessageId, - Mission = null! // EF will populate if included - }; - d = await _dialogueService.CreateDialogueAsync(d); - return CreatedAtAction(nameof(GetByMission), new { missionId = d.MissionId }, d); - } -} +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/dialogue")] +[Authorize] +public class DialogueController : ControllerBase +{ + private readonly IDialogueService _dialogueService; + + public DialogueController(IDialogueService dialogueService) + { + _dialogueService = dialogueService; + } + + [HttpGet("mission/{missionId:guid}")] + public async Task GetByMission(Guid missionId) + { + var d = await _dialogueService.GetDialogueByMissionIdAsync(missionId); + return d == null ? NotFound() : Ok(d); + } + + [HttpGet("message/{messageId:guid}")] + public async Task GetMessage(Guid messageId) + { + var m = await _dialogueService.GetDialogueMessageByIdAsync(messageId); + return m == null ? NotFound() : Ok(m); + } + + [HttpGet("message/{messageId:guid}/options")] + public async Task GetOptions(Guid messageId) + { + var opts = await _dialogueService.GetResponseOptionsAsync(messageId); + return Ok(opts); + } + + public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId); + + [HttpPost("message/{messageId:guid}/respond")] + public async Task Respond(Guid messageId, DialogueResponseRequest req) + { + var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId); + if (next == null) return Ok(new { end = true }); + return Ok(next); + } + + public class CreateDialogueRequest + { + public Guid MissionId { get; set; } + public Guid InitialDialogueMessageId { get; set; } + public Guid InterimDialogueMessageId { get; set; } + public Guid EndDialogueMessageId { get; set; } + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreateDialogueRequest dto) + { + var d = new Dialogue + { + Id = Guid.NewGuid(), + MissionId = dto.MissionId, + InitialDialogueMessageId = dto.InitialDialogueMessageId, + InterimDialogueMessageId = dto.InterimDialogueMessageId, + EndDialogueMessageId = dto.EndDialogueMessageId, + Mission = null! // EF will populate if included + }; + d = await _dialogueService.CreateDialogueAsync(d); + return CreatedAtAction(nameof(GetByMission), new { missionId = d.MissionId }, d); + } +} diff --git a/LctMonolith/Controllers/InventoryController.cs b/LctMonolith/Controllers/InventoryController.cs index 56840ba..459f5ec 100644 --- a/LctMonolith/Controllers/InventoryController.cs +++ b/LctMonolith/Controllers/InventoryController.cs @@ -1,34 +1,34 @@ -using System.Security.Claims; -using LctMonolith.Services.Contracts; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/inventory")] -[Authorize] -public class InventoryController : ControllerBase -{ - private readonly IInventoryService _inventoryService; - public InventoryController(IInventoryService inventoryService) => _inventoryService = inventoryService; - - private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); - - /// Get inventory for current authenticated user. - [HttpGet] - public async Task GetMine(CancellationToken ct) - { - var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct); - return Ok(items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt })); - } - - /// Admin: get inventory for specific user. - [HttpGet("user/{userId:guid}")] - [Authorize(Roles = "Admin")] - public async Task GetByUser(Guid userId, CancellationToken ct) - { - var items = await _inventoryService.GetStoreInventoryAsync(userId, ct); - return Ok(items); - } -} +using System.Security.Claims; +using LctMonolith.Services.Contracts; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/inventory")] +[Authorize] +public class InventoryController : ControllerBase +{ + private readonly IInventoryService _inventoryService; + public InventoryController(IInventoryService inventoryService) => _inventoryService = inventoryService; + + private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + /// Get inventory for current authenticated user. + [HttpGet] + public async Task GetMine(CancellationToken ct) + { + var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct); + return Ok(items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt })); + } + + /// Admin: get inventory for specific user. + [HttpGet("user/{userId:guid}")] + [Authorize(Roles = "Admin")] + public async Task GetByUser(Guid userId, CancellationToken ct) + { + var items = await _inventoryService.GetStoreInventoryAsync(userId, ct); + return Ok(items); + } +} diff --git a/LctMonolith/Controllers/MissionCategoriesController.cs b/LctMonolith/Controllers/MissionCategoriesController.cs index 4279446..5c92c23 100644 --- a/LctMonolith/Controllers/MissionCategoriesController.cs +++ b/LctMonolith/Controllers/MissionCategoriesController.cs @@ -1,58 +1,58 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/mission-categories")] -[Authorize] -public class MissionCategoriesController : ControllerBase -{ - private readonly IMissionCategoryService _service; - public MissionCategoriesController(IMissionCategoryService service) => _service = service; - - [HttpGet] - public async Task GetAll() - { - var list = await _service.GetAllCategoriesAsync(); - return Ok(list); - } - - [HttpGet("{id:guid}")] - public async Task Get(Guid id) - { - var c = await _service.GetCategoryByIdAsync(id); - return c == null ? NotFound() : Ok(c); - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreateMissionCategoryDto dto) - { - var c = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title }); - return CreatedAtAction(nameof(Get), new { id = c.Id }, c); - } - - [HttpPut("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Update(Guid id, CreateMissionCategoryDto dto) - { - var c = await _service.GetCategoryByIdAsync(id); - if (c == null) return NotFound(); - c.Title = dto.Title; - await _service.UpdateCategoryAsync(c); - return Ok(c); - } - - [HttpDelete("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Delete(Guid id) - { - var ok = await _service.DeleteCategoryAsync(id); - return ok ? NoContent() : NotFound(); - } -} - +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/mission-categories")] +[Authorize] +public class MissionCategoriesController : ControllerBase +{ + private readonly IMissionCategoryService _service; + public MissionCategoriesController(IMissionCategoryService service) => _service = service; + + [HttpGet] + public async Task GetAll() + { + var list = await _service.GetAllCategoriesAsync(); + return Ok(list); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var c = await _service.GetCategoryByIdAsync(id); + return c == null ? NotFound() : Ok(c); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreateMissionCategoryDto dto) + { + var c = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title }); + return CreatedAtAction(nameof(Get), new { id = c.Id }, c); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update(Guid id, CreateMissionCategoryDto dto) + { + var c = await _service.GetCategoryByIdAsync(id); + if (c == null) return NotFound(); + c.Title = dto.Title; + await _service.UpdateCategoryAsync(c); + return Ok(c); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Delete(Guid id) + { + var ok = await _service.DeleteCategoryAsync(id); + return ok ? NoContent() : NotFound(); + } +} + diff --git a/LctMonolith/Controllers/MissionsController.cs b/LctMonolith/Controllers/MissionsController.cs index 3eaeef6..7c7b561 100644 --- a/LctMonolith/Controllers/MissionsController.cs +++ b/LctMonolith/Controllers/MissionsController.cs @@ -1,112 +1,112 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/missions")] -[Authorize] -public class MissionsController : ControllerBase -{ - private readonly IMissionService _missions; - private readonly IRuleValidationService _rules; - - public MissionsController(IMissionService missions, IRuleValidationService rules) - { - _missions = missions; - _rules = rules; - } - - [HttpGet("{id:guid}")] - public async Task Get(Guid id) - { - var m = await _missions.GetMissionByIdAsync(id); - return m == null ? NotFound() : Ok(m); - } - - [HttpGet("category/{categoryId:guid}")] - public async Task ByCategory(Guid categoryId) - { - var list = await _missions.GetMissionsByCategoryAsync(categoryId); - return Ok(list); - } - - [HttpGet("player/{playerId:guid}/available")] - public async Task Available(Guid playerId) - { - var list = await _missions.GetAvailableMissionsForPlayerAsync(playerId); - return Ok(list); - } - - [HttpGet("{id:guid}/rank-rules")] - public async Task RankRules(Guid id) - { - var rules = await _rules.GetApplicableRankRulesAsync(id); - return Ok(rules); - } - - public class CreateMissionRequest - { - public string Title { get; set; } = string.Empty; - public string? Description { get; set; } - public Guid MissionCategoryId { get; set; } - public Guid? ParentMissionId { get; set; } - public int ExpReward { get; set; } - public int ManaReward { get; set; } - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreateMissionRequest dto) - { - var mission = new Mission - { - Title = dto.Title, - Description = dto.Description ?? string.Empty, - MissionCategoryId = dto.MissionCategoryId, - ParentMissionId = dto.ParentMissionId, - ExpReward = dto.ExpReward, - ManaReward = dto.ManaReward - }; - mission = await _missions.CreateMissionAsync(mission); - return CreatedAtAction(nameof(Get), new { id = mission.Id }, mission); - } - - [HttpPut("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Update(Guid id, CreateMissionRequest dto) - { - var existing = await _missions.GetMissionByIdAsync(id); - if (existing == null) return NotFound(); - existing.Title = dto.Title; - existing.Description = dto.Description ?? string.Empty; - existing.MissionCategoryId = dto.MissionCategoryId; - existing.ParentMissionId = dto.ParentMissionId; - existing.ExpReward = dto.ExpReward; - existing.ManaReward = dto.ManaReward; - await _missions.UpdateMissionAsync(existing); - return Ok(existing); - } - - [HttpDelete("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Delete(Guid id) - { - var ok = await _missions.DeleteMissionAsync(id); - return ok ? NoContent() : NotFound(); - } - - public record CompleteMissionRequest(Guid PlayerId, object? Proof); - - [HttpPost("{missionId:guid}/complete")] - public async Task Complete(Guid missionId, CompleteMissionRequest r) - { - var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof); - if (!result.Success) return BadRequest(result); - return Ok(result); - } -} - +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/missions")] +[Authorize] +public class MissionsController : ControllerBase +{ + private readonly IMissionService _missions; + private readonly IRuleValidationService _rules; + + public MissionsController(IMissionService missions, IRuleValidationService rules) + { + _missions = missions; + _rules = rules; + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var m = await _missions.GetMissionByIdAsync(id); + return m == null ? NotFound() : Ok(m); + } + + [HttpGet("category/{categoryId:guid}")] + public async Task ByCategory(Guid categoryId) + { + var list = await _missions.GetMissionsByCategoryAsync(categoryId); + return Ok(list); + } + + [HttpGet("player/{playerId:guid}/available")] + public async Task Available(Guid playerId) + { + var list = await _missions.GetAvailableMissionsForPlayerAsync(playerId); + return Ok(list); + } + + [HttpGet("{id:guid}/rank-rules")] + public async Task RankRules(Guid id) + { + var rules = await _rules.GetApplicableRankRulesAsync(id); + return Ok(rules); + } + + public class CreateMissionRequest + { + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public Guid MissionCategoryId { get; set; } + public Guid? ParentMissionId { get; set; } + public int ExpReward { get; set; } + public int ManaReward { get; set; } + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreateMissionRequest dto) + { + var mission = new Mission + { + Title = dto.Title, + Description = dto.Description ?? string.Empty, + MissionCategoryId = dto.MissionCategoryId, + ParentMissionId = dto.ParentMissionId, + ExpReward = dto.ExpReward, + ManaReward = dto.ManaReward + }; + mission = await _missions.CreateMissionAsync(mission); + return CreatedAtAction(nameof(Get), new { id = mission.Id }, mission); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update(Guid id, CreateMissionRequest dto) + { + var existing = await _missions.GetMissionByIdAsync(id); + if (existing == null) return NotFound(); + existing.Title = dto.Title; + existing.Description = dto.Description ?? string.Empty; + existing.MissionCategoryId = dto.MissionCategoryId; + existing.ParentMissionId = dto.ParentMissionId; + existing.ExpReward = dto.ExpReward; + existing.ManaReward = dto.ManaReward; + await _missions.UpdateMissionAsync(existing); + return Ok(existing); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Delete(Guid id) + { + var ok = await _missions.DeleteMissionAsync(id); + return ok ? NoContent() : NotFound(); + } + + public record CompleteMissionRequest(Guid PlayerId, object? Proof); + + [HttpPost("{missionId:guid}/complete")] + public async Task Complete(Guid missionId, CompleteMissionRequest r) + { + var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof); + if (!result.Success) return BadRequest(result); + return Ok(result); + } +} + diff --git a/LctMonolith/Controllers/NotificationsController.cs b/LctMonolith/Controllers/NotificationsController.cs index 193ec56..83dfe14 100644 --- a/LctMonolith/Controllers/NotificationsController.cs +++ b/LctMonolith/Controllers/NotificationsController.cs @@ -1,58 +1,58 @@ -using System.Security.Claims; -using LctMonolith.Services; -using LctMonolith.Services.Contracts; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -/// -/// In-app user notifications API. -/// -[ApiController] -[Route("api/notifications")] -[Authorize] -public class NotificationsController : ControllerBase -{ - private readonly INotificationService _notifications; - - public NotificationsController(INotificationService notifications) - { - _notifications = notifications; - } - - private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); - - /// Get up to 100 unread notifications. - [HttpGet("unread")] - public async Task GetUnread(CancellationToken ct) - { - var list = await _notifications.GetUnreadAsync(GetUserId(), ct); - return Ok(list); - } - - /// Get recent notifications (paged by take). - [HttpGet] - public async Task GetAll([FromQuery] int take = 100, CancellationToken ct = default) - { - var list = await _notifications.GetAllAsync(GetUserId(), take, ct); - return Ok(list); - } - - /// Mark a notification as read. - [HttpPost("mark/{id:guid}")] - public async Task MarkRead(Guid id, CancellationToken ct) - { - await _notifications.MarkReadAsync(GetUserId(), id, ct); - return NoContent(); - } - - /// Mark all notifications as read. - [HttpPost("mark-all")] - public async Task MarkAll(CancellationToken ct) - { - var cnt = await _notifications.MarkAllReadAsync(GetUserId(), ct); - return Ok(new { updated = cnt }); - } -} - +using System.Security.Claims; +using LctMonolith.Services; +using LctMonolith.Services.Contracts; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +/// +/// In-app user notifications API. +/// +[ApiController] +[Route("api/notifications")] +[Authorize] +public class NotificationsController : ControllerBase +{ + private readonly INotificationService _notifications; + + public NotificationsController(INotificationService notifications) + { + _notifications = notifications; + } + + private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + /// Get up to 100 unread notifications. + [HttpGet("unread")] + public async Task GetUnread(CancellationToken ct) + { + var list = await _notifications.GetUnreadAsync(GetUserId(), ct); + return Ok(list); + } + + /// Get recent notifications (paged by take). + [HttpGet] + public async Task GetAll([FromQuery] int take = 100, CancellationToken ct = default) + { + var list = await _notifications.GetAllAsync(GetUserId(), take, ct); + return Ok(list); + } + + /// Mark a notification as read. + [HttpPost("mark/{id:guid}")] + public async Task MarkRead(Guid id, CancellationToken ct) + { + await _notifications.MarkReadAsync(GetUserId(), id, ct); + return NoContent(); + } + + /// Mark all notifications as read. + [HttpPost("mark-all")] + public async Task MarkAll(CancellationToken ct) + { + var cnt = await _notifications.MarkAllReadAsync(GetUserId(), ct); + return Ok(new { updated = cnt }); + } +} + diff --git a/LctMonolith/Controllers/PlayersController.cs b/LctMonolith/Controllers/PlayersController.cs index 2bc19cb..a097243 100644 --- a/LctMonolith/Controllers/PlayersController.cs +++ b/LctMonolith/Controllers/PlayersController.cs @@ -1,78 +1,78 @@ -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/players")] -[Authorize] -public class PlayersController : ControllerBase -{ - private readonly IPlayerService _playerService; - private readonly IProgressTrackingService _progressService; - - public PlayersController(IPlayerService playerService, IProgressTrackingService progressService) - { - _playerService = playerService; - _progressService = progressService; - } - - [HttpGet("{playerId:guid}")] - public async Task GetPlayer(Guid playerId) - { - var player = await _playerService.GetPlayerWithProgressAsync(playerId); - return Ok(player); - } - - [HttpGet("user/{userId:guid}")] - public async Task GetByUser(Guid userId) - { - var p = await _playerService.GetPlayerByUserIdAsync(userId.ToString()); - if (p == null) return NotFound(); - return Ok(p); - } - - public class CreatePlayerRequest { public Guid UserId { get; set; } public string Username { get; set; } = string.Empty; } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreatePlayerRequest req) - { - var p = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username); - return CreatedAtAction(nameof(GetPlayer), new { playerId = p.Id }, p); - } - - public record AdjustValueRequest(int Value); - - [HttpPost("{playerId:guid}/experience")] - [Authorize(Roles = "Admin")] // manual adjust - public async Task AddExperience(Guid playerId, AdjustValueRequest r) - { - var p = await _playerService.AddPlayerExperienceAsync(playerId, r.Value); - return Ok(new { p.Id, p.Experience }); - } - - [HttpPost("{playerId:guid}/mana")] - [Authorize(Roles = "Admin")] // manual adjust - public async Task AddMana(Guid playerId, AdjustValueRequest r) - { - var p = await _playerService.AddPlayerManaAsync(playerId, r.Value); - return Ok(new { p.Id, p.Mana }); - } - - [HttpGet("top")] - public async Task GetTop([FromQuery] int count = 10) - { - var list = await _playerService.GetTopPlayersAsync(count, TimeSpan.FromDays(30)); - return Ok(list); - } - - [HttpGet("{playerId:guid}/progress")] - public async Task GetProgress(Guid playerId) - { - var prog = await _progressService.GetPlayerOverallProgressAsync(playerId); - return Ok(prog); - } -} - +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/players")] +[Authorize] +public class PlayersController : ControllerBase +{ + private readonly IPlayerService _playerService; + private readonly IProgressTrackingService _progressService; + + public PlayersController(IPlayerService playerService, IProgressTrackingService progressService) + { + _playerService = playerService; + _progressService = progressService; + } + + [HttpGet("{playerId:guid}")] + public async Task GetPlayer(Guid playerId) + { + var player = await _playerService.GetPlayerWithProgressAsync(playerId); + return Ok(player); + } + + [HttpGet("user/{userId:guid}")] + public async Task GetByUser(Guid userId) + { + var p = await _playerService.GetPlayerByUserIdAsync(userId.ToString()); + if (p == null) return NotFound(); + return Ok(p); + } + + public class CreatePlayerRequest { public Guid UserId { get; set; } public string Username { get; set; } = string.Empty; } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreatePlayerRequest req) + { + var p = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username); + return CreatedAtAction(nameof(GetPlayer), new { playerId = p.Id }, p); + } + + public record AdjustValueRequest(int Value); + + [HttpPost("{playerId:guid}/experience")] + [Authorize(Roles = "Admin")] // manual adjust + public async Task AddExperience(Guid playerId, AdjustValueRequest r) + { + var p = await _playerService.AddPlayerExperienceAsync(playerId, r.Value); + return Ok(new { p.Id, p.Experience }); + } + + [HttpPost("{playerId:guid}/mana")] + [Authorize(Roles = "Admin")] // manual adjust + public async Task AddMana(Guid playerId, AdjustValueRequest r) + { + var p = await _playerService.AddPlayerManaAsync(playerId, r.Value); + return Ok(new { p.Id, p.Mana }); + } + + [HttpGet("top")] + public async Task GetTop([FromQuery] int count = 10) + { + var list = await _playerService.GetTopPlayersAsync(count, TimeSpan.FromDays(30)); + return Ok(list); + } + + [HttpGet("{playerId:guid}/progress")] + public async Task GetProgress(Guid playerId) + { + var prog = await _progressService.GetPlayerOverallProgressAsync(playerId); + return Ok(prog); + } +} + diff --git a/LctMonolith/Controllers/ProfileController.cs b/LctMonolith/Controllers/ProfileController.cs index d718a0c..a59ca08 100644 --- a/LctMonolith/Controllers/ProfileController.cs +++ b/LctMonolith/Controllers/ProfileController.cs @@ -1,67 +1,67 @@ -using System.Security.Claims; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/profile")] -[Authorize] -public class ProfileController : ControllerBase -{ - private readonly IProfileService _profiles; - public ProfileController(IProfileService profiles) => _profiles = profiles; - - private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); - - public class UpdateProfileDto - { - public string? FirstName { get; set; } - public string? LastName { get; set; } - public DateOnly? BirthDate { get; set; } - public string? About { get; set; } - public string? Location { get; set; } - } - - [HttpGet("me")] - public async Task GetMe(CancellationToken ct) - { - var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct); - if (p == null) return NotFound(); - return Ok(p); - } - - [HttpGet("{userId:guid}")] - [Authorize(Roles = "Admin")] - public async Task GetByUser(Guid userId, CancellationToken ct) - { - var p = await _profiles.GetByUserIdAsync(userId, ct); - return p == null ? NotFound() : Ok(p); - } - - [HttpPut] - public async Task Upsert(UpdateProfileDto dto, CancellationToken ct) - { - var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct); - return Ok(p); - } - - [HttpPost("avatar")] - [RequestSizeLimit(7_000_000)] // ~7MB - public async Task UploadAvatar(IFormFile file, CancellationToken ct) - { - if (file == null || file.Length == 0) return BadRequest("File required"); - await using var stream = file.OpenReadStream(); - var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct); - return Ok(new { p.AvatarUrl }); - } - - [HttpDelete("avatar")] - public async Task DeleteAvatar(CancellationToken ct) - { - var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct); - return ok ? NoContent() : NotFound(); - } -} +using System.Security.Claims; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/profile")] +[Authorize] +public class ProfileController : ControllerBase +{ + private readonly IProfileService _profiles; + public ProfileController(IProfileService profiles) => _profiles = profiles; + + private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + public class UpdateProfileDto + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + public DateOnly? BirthDate { get; set; } + public string? About { get; set; } + public string? Location { get; set; } + } + + [HttpGet("me")] + public async Task GetMe(CancellationToken ct) + { + var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct); + if (p == null) return NotFound(); + return Ok(p); + } + + [HttpGet("{userId:guid}")] + [Authorize(Roles = "Admin")] + public async Task GetByUser(Guid userId, CancellationToken ct) + { + var p = await _profiles.GetByUserIdAsync(userId, ct); + return p == null ? NotFound() : Ok(p); + } + + [HttpPut] + public async Task Upsert(UpdateProfileDto dto, CancellationToken ct) + { + var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct); + return Ok(p); + } + + [HttpPost("avatar")] + [RequestSizeLimit(7_000_000)] // ~7MB + public async Task UploadAvatar(IFormFile file, CancellationToken ct) + { + if (file == null || file.Length == 0) return BadRequest("File required"); + await using var stream = file.OpenReadStream(); + var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct); + return Ok(new { p.AvatarUrl }); + } + + [HttpDelete("avatar")] + public async Task DeleteAvatar(CancellationToken ct) + { + var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct); + return ok ? NoContent() : NotFound(); + } +} diff --git a/LctMonolith/Controllers/RanksController.cs b/LctMonolith/Controllers/RanksController.cs index 3c1b98b..b5bfd73 100644 --- a/LctMonolith/Controllers/RanksController.cs +++ b/LctMonolith/Controllers/RanksController.cs @@ -1,72 +1,72 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/ranks")] -[Authorize] -public class RanksController : ControllerBase -{ - private readonly IRankService _rankService; - private readonly IRuleValidationService _ruleValidation; - - public RanksController(IRankService rankService, IRuleValidationService ruleValidation) - { - _rankService = rankService; - _ruleValidation = ruleValidation; - } - - [HttpGet] - public async Task GetAll() - { - var ranks = await _rankService.GetAllRanksAsync(); - return Ok(ranks); - } - - [HttpGet("{id:guid}")] - public async Task Get(Guid id) - { - var r = await _rankService.GetRankByIdAsync(id); - return r == null ? NotFound() : Ok(r); - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreateRankDto dto) - { - var rank = await _rankService.CreateRankAsync(new Rank { Title = dto.Title, ExpNeeded = dto.ExpNeeded }); - return CreatedAtAction(nameof(Get), new { id = rank.Id }, rank); - } - - [HttpPut("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Update(Guid id, CreateRankDto dto) - { - var r = await _rankService.GetRankByIdAsync(id); - if (r == null) return NotFound(); - r.Title = dto.Title; - r.ExpNeeded = dto.ExpNeeded; - await _rankService.UpdateRankAsync(r); - return Ok(r); - } - - [HttpDelete("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Delete(Guid id) - { - var ok = await _rankService.DeleteRankAsync(id); - return ok ? NoContent() : NotFound(); - } - - [HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")] - public async Task CanAdvance(Guid playerId, Guid targetRankId) - { - var ok = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId); - return Ok(new { playerId, targetRankId, canAdvance = ok }); - } -} - +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/ranks")] +[Authorize] +public class RanksController : ControllerBase +{ + private readonly IRankService _rankService; + private readonly IRuleValidationService _ruleValidation; + + public RanksController(IRankService rankService, IRuleValidationService ruleValidation) + { + _rankService = rankService; + _ruleValidation = ruleValidation; + } + + [HttpGet] + public async Task GetAll() + { + var ranks = await _rankService.GetAllRanksAsync(); + return Ok(ranks); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var r = await _rankService.GetRankByIdAsync(id); + return r == null ? NotFound() : Ok(r); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreateRankDto dto) + { + var rank = await _rankService.CreateRankAsync(new Rank { Title = dto.Title, ExpNeeded = dto.ExpNeeded }); + return CreatedAtAction(nameof(Get), new { id = rank.Id }, rank); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update(Guid id, CreateRankDto dto) + { + var r = await _rankService.GetRankByIdAsync(id); + if (r == null) return NotFound(); + r.Title = dto.Title; + r.ExpNeeded = dto.ExpNeeded; + await _rankService.UpdateRankAsync(r); + return Ok(r); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Delete(Guid id) + { + var ok = await _rankService.DeleteRankAsync(id); + return ok ? NoContent() : NotFound(); + } + + [HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")] + public async Task CanAdvance(Guid playerId, Guid targetRankId) + { + var ok = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId); + return Ok(new { playerId, targetRankId, canAdvance = ok }); + } +} + diff --git a/LctMonolith/Controllers/RewardController.cs b/LctMonolith/Controllers/RewardController.cs index c608793..40c216e 100644 --- a/LctMonolith/Controllers/RewardController.cs +++ b/LctMonolith/Controllers/RewardController.cs @@ -1,65 +1,65 @@ -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/rewards")] -[Authorize] -public class RewardController : ControllerBase -{ - private readonly IRewardService _rewardService; - - public RewardController(IRewardService rewardService) - { - _rewardService = rewardService; - } - - /// List skill rewards configured for a mission. - [HttpGet("mission/{missionId:guid}/skills")] - public async Task GetMissionSkillRewards(Guid missionId) - { - var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId); - return Ok(rewards.Select(r => new { r.SkillId, r.Value })); - } - - /// List item rewards configured for a mission. - [HttpGet("mission/{missionId:guid}/items")] - public async Task GetMissionItemRewards(Guid missionId) - { - var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId); - return Ok(rewards.Select(r => new { r.ItemId })); - } - - /// Check if mission rewards can be claimed by player (missionId used as rewardId). - [HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")] - public async Task CanClaim(Guid missionId, Guid playerId) - { - var can = await _rewardService.CanClaimRewardAsync(missionId, playerId); - return Ok(new { missionId, playerId, canClaim = can }); - } - - public record ClaimRewardRequest(Guid PlayerId); - - /// Claim mission rewards if available (idempotent on already claimed). - [HttpPost("mission/{missionId:guid}/claim")] - public async Task Claim(Guid missionId, ClaimRewardRequest req) - { - var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId); - if (!can) return Conflict(new { message = "Rewards already claimed or mission not completed" }); - await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId); - return Ok(new { missionId, req.PlayerId, status = "claimed" }); - } - - public record ForceDistributeRequest(Guid PlayerId); - - /// Admin: force distribute rewards regardless of previous state (may duplicate). Use cautiously. - [HttpPost("mission/{missionId:guid}/force-distribute")] - [Authorize(Roles = "Admin")] - public async Task ForceDistribute(Guid missionId, ForceDistributeRequest req) - { - await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId); - return Ok(new { missionId, req.PlayerId, status = "forced" }); - } -} +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/rewards")] +[Authorize] +public class RewardController : ControllerBase +{ + private readonly IRewardService _rewardService; + + public RewardController(IRewardService rewardService) + { + _rewardService = rewardService; + } + + /// List skill rewards configured for a mission. + [HttpGet("mission/{missionId:guid}/skills")] + public async Task GetMissionSkillRewards(Guid missionId) + { + var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId); + return Ok(rewards.Select(r => new { r.SkillId, r.Value })); + } + + /// List item rewards configured for a mission. + [HttpGet("mission/{missionId:guid}/items")] + public async Task GetMissionItemRewards(Guid missionId) + { + var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId); + return Ok(rewards.Select(r => new { r.ItemId })); + } + + /// Check if mission rewards can be claimed by player (missionId used as rewardId). + [HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")] + public async Task CanClaim(Guid missionId, Guid playerId) + { + var can = await _rewardService.CanClaimRewardAsync(missionId, playerId); + return Ok(new { missionId, playerId, canClaim = can }); + } + + public record ClaimRewardRequest(Guid PlayerId); + + /// Claim mission rewards if available (idempotent on already claimed). + [HttpPost("mission/{missionId:guid}/claim")] + public async Task Claim(Guid missionId, ClaimRewardRequest req) + { + var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId); + if (!can) return Conflict(new { message = "Rewards already claimed or mission not completed" }); + await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId); + return Ok(new { missionId, req.PlayerId, status = "claimed" }); + } + + public record ForceDistributeRequest(Guid PlayerId); + + /// Admin: force distribute rewards regardless of previous state (may duplicate). Use cautiously. + [HttpPost("mission/{missionId:guid}/force-distribute")] + [Authorize(Roles = "Admin")] + public async Task ForceDistribute(Guid missionId, ForceDistributeRequest req) + { + await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId); + return Ok(new { missionId, req.PlayerId, status = "forced" }); + } +} diff --git a/LctMonolith/Controllers/SkillsController.cs b/LctMonolith/Controllers/SkillsController.cs index c269a15..515d392 100644 --- a/LctMonolith/Controllers/SkillsController.cs +++ b/LctMonolith/Controllers/SkillsController.cs @@ -1,79 +1,79 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -[ApiController] -[Route("api/skills")] -[Authorize] -public class SkillsController : ControllerBase -{ - private readonly ISkillService _skillService; - - public SkillsController(ISkillService skillService) - { - _skillService = skillService; - } - - [HttpGet] - public async Task GetAll() - { - var list = await _skillService.GetAllSkillsAsync(); - return Ok(list); - } - - [HttpGet("{id:guid}")] - public async Task Get(Guid id) - { - var s = await _skillService.GetSkillByIdAsync(id); - return s == null ? NotFound() : Ok(s); - } - - [HttpPost] - [Authorize(Roles = "Admin")] - public async Task Create(CreateSkillDto dto) - { - var skill = await _skillService.CreateSkillAsync(new Skill { Title = dto.Title }); - return CreatedAtAction(nameof(Get), new { id = skill.Id }, skill); - } - - [HttpPut("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Update(Guid id, CreateSkillDto dto) - { - var s = await _skillService.GetSkillByIdAsync(id); - if (s == null) return NotFound(); - s.Title = dto.Title; - await _skillService.UpdateSkillAsync(s); - return Ok(s); - } - - [HttpDelete("{id:guid}")] - [Authorize(Roles = "Admin")] - public async Task Delete(Guid id) - { - var ok = await _skillService.DeleteSkillAsync(id); - return ok ? NoContent() : NotFound(); - } - - [HttpGet("player/{playerId:guid}")] - public async Task PlayerSkills(Guid playerId) - { - var list = await _skillService.GetPlayerSkillsAsync(playerId); - return Ok(list); - } - - public record UpdatePlayerSkillRequest(int Level); - - [HttpPost("player/{playerId:guid}/{skillId:guid}")] - [Authorize(Roles = "Admin")] - public async Task UpdatePlayerSkill(Guid playerId, Guid skillId, UpdatePlayerSkillRequest r) - { - var ps = await _skillService.UpdatePlayerSkillAsync(playerId, skillId, r.Level); - return Ok(new { ps.PlayerId, ps.SkillId, ps.Score }); - } -} - +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +[ApiController] +[Route("api/skills")] +[Authorize] +public class SkillsController : ControllerBase +{ + private readonly ISkillService _skillService; + + public SkillsController(ISkillService skillService) + { + _skillService = skillService; + } + + [HttpGet] + public async Task GetAll() + { + var list = await _skillService.GetAllSkillsAsync(); + return Ok(list); + } + + [HttpGet("{id:guid}")] + public async Task Get(Guid id) + { + var s = await _skillService.GetSkillByIdAsync(id); + return s == null ? NotFound() : Ok(s); + } + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task Create(CreateSkillDto dto) + { + var skill = await _skillService.CreateSkillAsync(new Skill { Title = dto.Title }); + return CreatedAtAction(nameof(Get), new { id = skill.Id }, skill); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Update(Guid id, CreateSkillDto dto) + { + var s = await _skillService.GetSkillByIdAsync(id); + if (s == null) return NotFound(); + s.Title = dto.Title; + await _skillService.UpdateSkillAsync(s); + return Ok(s); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task Delete(Guid id) + { + var ok = await _skillService.DeleteSkillAsync(id); + return ok ? NoContent() : NotFound(); + } + + [HttpGet("player/{playerId:guid}")] + public async Task PlayerSkills(Guid playerId) + { + var list = await _skillService.GetPlayerSkillsAsync(playerId); + return Ok(list); + } + + public record UpdatePlayerSkillRequest(int Level); + + [HttpPost("player/{playerId:guid}/{skillId:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdatePlayerSkill(Guid playerId, Guid skillId, UpdatePlayerSkillRequest r) + { + var ps = await _skillService.UpdatePlayerSkillAsync(playerId, skillId, r.Level); + return Ok(new { ps.PlayerId, ps.SkillId, ps.Score }); + } +} + diff --git a/LctMonolith/Controllers/StoreController.cs b/LctMonolith/Controllers/StoreController.cs index d955aaf..8689328 100644 --- a/LctMonolith/Controllers/StoreController.cs +++ b/LctMonolith/Controllers/StoreController.cs @@ -1,43 +1,43 @@ -using System.Security.Claims; -using LctMonolith.Services; -using LctMonolith.Services.Contracts; -using LctMonolith.Services.Models; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace LctMonolith.Controllers; - -/// -/// Store endpoints for listing items and purchasing. -/// -[ApiController] -[Route("api/store")] -[Authorize] -public class StoreController : ControllerBase -{ - private readonly IStoreService _storeService; - - public StoreController(IStoreService storeService) - { - _storeService = storeService; - } - - private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); - - /// List active store items. - [HttpGet("items")] - public async Task GetItems(CancellationToken ct) - { - var items = await _storeService.GetActiveItemsAsync(ct); - return Ok(items); - } - - /// Purchase an item for the authenticated user. - [HttpPost("purchase")] - public async Task Purchase(PurchaseRequest req, CancellationToken ct) - { - var inv = await _storeService.PurchaseAsync(GetUserId(), req.ItemId, req.Quantity, ct); - return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt }); - } -} - +using System.Security.Claims; +using LctMonolith.Services; +using LctMonolith.Services.Contracts; +using LctMonolith.Services.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace LctMonolith.Controllers; + +/// +/// Store endpoints for listing items and purchasing. +/// +[ApiController] +[Route("api/store")] +[Authorize] +public class StoreController : ControllerBase +{ + private readonly IStoreService _storeService; + + public StoreController(IStoreService storeService) + { + _storeService = storeService; + } + + private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + /// List active store items. + [HttpGet("items")] + public async Task GetItems(CancellationToken ct) + { + var items = await _storeService.GetActiveItemsAsync(ct); + return Ok(items); + } + + /// Purchase an item for the authenticated user. + [HttpPost("purchase")] + public async Task Purchase(PurchaseRequest req, CancellationToken ct) + { + var inv = await _storeService.PurchaseAsync(GetUserId(), req.ItemId, req.Quantity, ct); + return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt }); + } +} + diff --git a/LctMonolith/Database/Data/AppDbContext.cs b/LctMonolith/Database/Data/AppDbContext.cs index 3c5df27..f97fba3 100644 --- a/LctMonolith/Database/Data/AppDbContext.cs +++ b/LctMonolith/Database/Data/AppDbContext.cs @@ -1,224 +1,224 @@ -using LctMonolith.Models.Database; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using EventLog = LctMonolith.Models.Database.EventLog; - -namespace LctMonolith.Database.Data; - -/// -/// Main EF Core database context for gamification module (PostgreSQL provider expected). -/// -public class AppDbContext : IdentityDbContext, Guid> -{ - public AppDbContext(DbContextOptions options) : base(options) { } - - // Rank related entities - public DbSet Ranks => Set(); - public DbSet RankMissionRules => Set(); - public DbSet RankSkillRules => Set(); - - // Mission related entities - public DbSet MissionCategories => Set(); - public DbSet Missions => Set(); - public DbSet PlayerMissions => Set(); - public DbSet MissionSkillRewards => Set(); - public DbSet MissionItemRewards => Set(); - public DbSet MissionRankRules => Set(); - - // Skill related entities - public DbSet Skills => Set(); - public DbSet PlayerSkills => Set(); - - // Dialogue related entities - public DbSet Dialogues => Set(); - public DbSet DialogueMessages => Set(); - public DbSet DialogueMessageResponseOptions => Set(); - - // Store and inventory - public DbSet StoreItems => Set(); - public DbSet UserInventoryItems => Set(); - public DbSet Transactions => Set(); - - // System entities - public DbSet EventLogs => Set(); - public DbSet RefreshTokens => Set(); - public DbSet Notifications => Set(); - - // Core profile / player chain - public DbSet Players => Set(); - public DbSet Profiles => Set(); - - protected override void OnModelCreating(ModelBuilder b) - { - base.OnModelCreating(b); - - // Player configuration - b.Entity() - .HasIndex(p => p.UserId) - .IsUnique(); - b.Entity() - .HasOne() - .WithOne() - .HasForeignKey(p => p.UserId) - .IsRequired(); - - // Rank configurations - b.Entity() - .HasIndex(r => r.ExpNeeded) - .IsUnique(); - b.Entity() - .HasIndex(r => r.Title) - .IsUnique(); - - // Skill configurations - b.Entity() - .HasIndex(s => s.Title) - .IsUnique(); - - // MissionCategory configurations - b.Entity() - .HasIndex(mc => mc.Title) - .IsUnique(); - - // Mission configurations - b.Entity() - .HasOne(m => m.MissionCategory) - .WithMany(mc => mc.Missions) - .HasForeignKey(m => m.MissionCategoryId) - .IsRequired(); - b.Entity() - .HasOne(m => m.ParentMission) - .WithMany(m => m.ChildMissions) - .HasForeignKey(m => m.ParentMissionId) - .IsRequired(false); - // Dialogue relationship for Mission - b.Entity() - .HasOne(m => m.Dialogue) - .WithOne(d => d.Mission) - .HasForeignKey(m => m.DialogueId) - .IsRequired(false); - - // MissionRankRule configurations - b.Entity() - .HasOne(mrr => mrr.Mission) - .WithMany(m => m.MissionRankRules) - .HasForeignKey(mrr => mrr.MissionId); - b.Entity() - .HasOne(mrr => mrr.Rank) - .WithMany(r => r.MissionRankRules) - .HasForeignKey(mrr => mrr.RankId); - - // MissionSkillReward configurations - b.Entity() - .HasKey(x => new { x.MissionId, x.SkillId }); - b.Entity() - .HasOne(msr => msr.Mission) - .WithMany(m => m.MissionSkillRewards) - .HasForeignKey(msr => msr.MissionId); - b.Entity() - .HasOne(msr => msr.Skill) - .WithMany(s => s.MissionSkillRewards) - .HasForeignKey(msr => msr.SkillId); - - // MissionItemReward configurations - b.Entity() - .HasOne(mir => mir.Mission) - .WithMany(m => m.MissionItemRewards) - .HasForeignKey(mir => mir.MissionId); - - // RankMissionRule composite key - b.Entity().HasKey(x => new { x.RankId, x.MissionId }); - b.Entity() - .HasOne(rmr => rmr.Rank) - .WithMany(r => r.RankMissionRules) - .HasForeignKey(rmr => rmr.RankId); - b.Entity() - .HasOne(rmr => rmr.Mission) - .WithMany(m => m.RankMissionRules) - .HasForeignKey(rmr => rmr.MissionId); - - // RankSkillRule composite key - b.Entity().HasKey(x => new { x.RankId, x.SkillId }); - b.Entity() - .HasOne(rsr => rsr.Rank) - .WithMany(r => r.RankSkillRules) - .HasForeignKey(rsr => rsr.RankId); - b.Entity() - .HasOne(rsr => rsr.Skill) - .WithMany(s => s.RankSkillRules) - .HasForeignKey(rsr => rsr.SkillId); - - // PlayerSkill composite key - b.Entity().HasKey(x => new { x.PlayerId, x.SkillId }); - b.Entity() - .HasOne(ps => ps.Player) - .WithMany(p => p.PlayerSkills) - .HasForeignKey(ps => ps.PlayerId); - b.Entity() - .HasOne(ps => ps.Skill) - .WithMany(s => s.PlayerSkills) - .HasForeignKey(ps => ps.SkillId); - - // PlayerMission composite key - b.Entity().HasKey(x => new { x.PlayerId, x.MissionId }); - b.Entity() - .HasOne(pm => pm.Player) - .WithMany(p => p.PlayerMissions) - .HasForeignKey(pm => pm.PlayerId); - b.Entity() - .HasOne(pm => pm.Mission) - .WithMany(m => m.PlayerMissions) - .HasForeignKey(pm => pm.MissionId); - - // Dialogue configurations - b.Entity() - .HasOne(d => d.Mission) - .WithOne(m => m.Dialogue) - .HasForeignKey(d => d.MissionId) - .IsRequired(); - - // DialogueMessage configurations - b.Entity() - .HasOne(dm => dm.InitialDialogue) - .WithMany() - .HasForeignKey(dm => dm.InitialDialogueId) - .IsRequired(false) - .OnDelete(DeleteBehavior.Restrict); - b.Entity() - .HasOne(dm => dm.InterimDialogue) - .WithMany() - .HasForeignKey(dm => dm.InterimDialogueId) - .IsRequired(false) - .OnDelete(DeleteBehavior.Restrict); - b.Entity() - .HasOne(dm => dm.EndDialogue) - .WithMany() - .HasForeignKey(dm => dm.EndDialogueId) - .IsRequired(false) - .OnDelete(DeleteBehavior.Restrict); - - // DialogueMessageResponseOption configurations - b.Entity() - .HasOne(dmro => dmro.ParentDialogueMessage) - .WithMany(dm => dm.DialogueMessageResponseOptions) - .HasForeignKey(dmro => dmro.ParentDialogueMessageId) - .IsRequired(); - b.Entity() - .HasOne(dmro => dmro.DestinationDialogueMessage) - .WithMany() - .HasForeignKey(dmro => dmro.DestinationDialogueMessageId) - .IsRequired(false) - .OnDelete(DeleteBehavior.Restrict); - - // Refresh token index unique - b.Entity().HasIndex(x => x.Token).IsUnique(); - - // ---------- Performance indexes ---------- - b.Entity().HasIndex(ps => ps.SkillId); - b.Entity().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt }); - b.Entity().HasIndex(i => i.IsActive); - b.Entity().HasIndex(t => new { t.UserId, t.CreatedAt }); - b.Entity().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt }); - } -} +using LctMonolith.Models.Database; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using EventLog = LctMonolith.Models.Database.EventLog; + +namespace LctMonolith.Database.Data; + +/// +/// Main EF Core database context for gamification module (PostgreSQL provider expected). +/// +public class AppDbContext : IdentityDbContext, Guid> +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + // Rank related entities + public DbSet Ranks => Set(); + public DbSet RankMissionRules => Set(); + public DbSet RankSkillRules => Set(); + + // Mission related entities + public DbSet MissionCategories => Set(); + public DbSet Missions => Set(); + public DbSet PlayerMissions => Set(); + public DbSet MissionSkillRewards => Set(); + public DbSet MissionItemRewards => Set(); + public DbSet MissionRankRules => Set(); + + // Skill related entities + public DbSet Skills => Set(); + public DbSet PlayerSkills => Set(); + + // Dialogue related entities + public DbSet Dialogues => Set(); + public DbSet DialogueMessages => Set(); + public DbSet DialogueMessageResponseOptions => Set(); + + // Store and inventory + public DbSet StoreItems => Set(); + public DbSet UserInventoryItems => Set(); + public DbSet Transactions => Set(); + + // System entities + public DbSet EventLogs => Set(); + public DbSet RefreshTokens => Set(); + public DbSet Notifications => Set(); + + // Core profile / player chain + public DbSet Players => Set(); + public DbSet Profiles => Set(); + + protected override void OnModelCreating(ModelBuilder b) + { + base.OnModelCreating(b); + + // Player configuration + b.Entity() + .HasIndex(p => p.UserId) + .IsUnique(); + b.Entity() + .HasOne() + .WithOne() + .HasForeignKey(p => p.UserId) + .IsRequired(); + + // Rank configurations + b.Entity() + .HasIndex(r => r.ExpNeeded) + .IsUnique(); + b.Entity() + .HasIndex(r => r.Title) + .IsUnique(); + + // Skill configurations + b.Entity() + .HasIndex(s => s.Title) + .IsUnique(); + + // MissionCategory configurations + b.Entity() + .HasIndex(mc => mc.Title) + .IsUnique(); + + // Mission configurations + b.Entity() + .HasOne(m => m.MissionCategory) + .WithMany(mc => mc.Missions) + .HasForeignKey(m => m.MissionCategoryId) + .IsRequired(); + b.Entity() + .HasOne(m => m.ParentMission) + .WithMany(m => m.ChildMissions) + .HasForeignKey(m => m.ParentMissionId) + .IsRequired(false); + // Dialogue relationship for Mission + b.Entity() + .HasOne(m => m.Dialogue) + .WithOne(d => d.Mission) + .HasForeignKey(m => m.DialogueId) + .IsRequired(false); + + // MissionRankRule configurations + b.Entity() + .HasOne(mrr => mrr.Mission) + .WithMany(m => m.MissionRankRules) + .HasForeignKey(mrr => mrr.MissionId); + b.Entity() + .HasOne(mrr => mrr.Rank) + .WithMany(r => r.MissionRankRules) + .HasForeignKey(mrr => mrr.RankId); + + // MissionSkillReward configurations + b.Entity() + .HasKey(x => new { x.MissionId, x.SkillId }); + b.Entity() + .HasOne(msr => msr.Mission) + .WithMany(m => m.MissionSkillRewards) + .HasForeignKey(msr => msr.MissionId); + b.Entity() + .HasOne(msr => msr.Skill) + .WithMany(s => s.MissionSkillRewards) + .HasForeignKey(msr => msr.SkillId); + + // MissionItemReward configurations + b.Entity() + .HasOne(mir => mir.Mission) + .WithMany(m => m.MissionItemRewards) + .HasForeignKey(mir => mir.MissionId); + + // RankMissionRule composite key + b.Entity().HasKey(x => new { x.RankId, x.MissionId }); + b.Entity() + .HasOne(rmr => rmr.Rank) + .WithMany(r => r.RankMissionRules) + .HasForeignKey(rmr => rmr.RankId); + b.Entity() + .HasOne(rmr => rmr.Mission) + .WithMany(m => m.RankMissionRules) + .HasForeignKey(rmr => rmr.MissionId); + + // RankSkillRule composite key + b.Entity().HasKey(x => new { x.RankId, x.SkillId }); + b.Entity() + .HasOne(rsr => rsr.Rank) + .WithMany(r => r.RankSkillRules) + .HasForeignKey(rsr => rsr.RankId); + b.Entity() + .HasOne(rsr => rsr.Skill) + .WithMany(s => s.RankSkillRules) + .HasForeignKey(rsr => rsr.SkillId); + + // PlayerSkill composite key + b.Entity().HasKey(x => new { x.PlayerId, x.SkillId }); + b.Entity() + .HasOne(ps => ps.Player) + .WithMany(p => p.PlayerSkills) + .HasForeignKey(ps => ps.PlayerId); + b.Entity() + .HasOne(ps => ps.Skill) + .WithMany(s => s.PlayerSkills) + .HasForeignKey(ps => ps.SkillId); + + // PlayerMission composite key + b.Entity().HasKey(x => new { x.PlayerId, x.MissionId }); + b.Entity() + .HasOne(pm => pm.Player) + .WithMany(p => p.PlayerMissions) + .HasForeignKey(pm => pm.PlayerId); + b.Entity() + .HasOne(pm => pm.Mission) + .WithMany(m => m.PlayerMissions) + .HasForeignKey(pm => pm.MissionId); + + // Dialogue configurations + b.Entity() + .HasOne(d => d.Mission) + .WithOne(m => m.Dialogue) + .HasForeignKey(d => d.MissionId) + .IsRequired(); + + // DialogueMessage configurations + b.Entity() + .HasOne(dm => dm.InitialDialogue) + .WithMany() + .HasForeignKey(dm => dm.InitialDialogueId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + b.Entity() + .HasOne(dm => dm.InterimDialogue) + .WithMany() + .HasForeignKey(dm => dm.InterimDialogueId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + b.Entity() + .HasOne(dm => dm.EndDialogue) + .WithMany() + .HasForeignKey(dm => dm.EndDialogueId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + + // DialogueMessageResponseOption configurations + b.Entity() + .HasOne(dmro => dmro.ParentDialogueMessage) + .WithMany(dm => dm.DialogueMessageResponseOptions) + .HasForeignKey(dmro => dmro.ParentDialogueMessageId) + .IsRequired(); + b.Entity() + .HasOne(dmro => dmro.DestinationDialogueMessage) + .WithMany() + .HasForeignKey(dmro => dmro.DestinationDialogueMessageId) + .IsRequired(false) + .OnDelete(DeleteBehavior.Restrict); + + // Refresh token index unique + b.Entity().HasIndex(x => x.Token).IsUnique(); + + // ---------- Performance indexes ---------- + b.Entity().HasIndex(ps => ps.SkillId); + b.Entity().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt }); + b.Entity().HasIndex(i => i.IsActive); + b.Entity().HasIndex(t => new { t.UserId, t.CreatedAt }); + b.Entity().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt }); + } +} diff --git a/LctMonolith/Database/Data/DbSeeder.cs b/LctMonolith/Database/Data/DbSeeder.cs index 56f7904..40a6722 100644 --- a/LctMonolith/Database/Data/DbSeeder.cs +++ b/LctMonolith/Database/Data/DbSeeder.cs @@ -1,47 +1,47 @@ -using LctMonolith.Models.Database; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Database.Data; - -/// -/// Development database seeder for initial ranks, competencies, sample store items. -/// Idempotent: checks existence before inserting. -/// -public static class DbSeeder -{ - public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default) - { - await db.Database.EnsureCreatedAsync(ct); - - if (!await db.Ranks.AnyAsync(ct)) - { - var ranks = new List - { - new() { Title = "Искатель", ExpNeeded = 0 }, - new() { Title = "Пилот-кандидат", ExpNeeded = 500 }, - new() { Title = "Принятый в экипаж", ExpNeeded = 1500 } - }; - db.Ranks.AddRange(ranks); - Log.Information("Seeded {Count} ranks", ranks.Count); - } - - if (!await db.Skills.AnyAsync(ct)) - { - var comps = new[] - { - "Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации" - }.Select(n => new Skill { Title = n }); - db.Skills.AddRange(comps); - Log.Information("Seeded competencies"); - } - - if (!await db.StoreItems.AnyAsync(ct)) - { - db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 }); - Log.Information("Seeded store items"); - } - - await db.SaveChangesAsync(ct); - } -} +using LctMonolith.Models.Database; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Database.Data; + +/// +/// Development database seeder for initial ranks, competencies, sample store items. +/// Idempotent: checks existence before inserting. +/// +public static class DbSeeder +{ + public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default) + { + await db.Database.EnsureCreatedAsync(ct); + + if (!await db.Ranks.AnyAsync(ct)) + { + var ranks = new List + { + new() { Title = "Искатель", ExpNeeded = 0 }, + new() { Title = "Пилот-кандидат", ExpNeeded = 500 }, + new() { Title = "Принятый в экипаж", ExpNeeded = 1500 } + }; + db.Ranks.AddRange(ranks); + Log.Information("Seeded {Count} ranks", ranks.Count); + } + + if (!await db.Skills.AnyAsync(ct)) + { + var comps = new[] + { + "Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации" + }.Select(n => new Skill { Title = n }); + db.Skills.AddRange(comps); + Log.Information("Seeded competencies"); + } + + if (!await db.StoreItems.AnyAsync(ct)) + { + db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 }); + Log.Information("Seeded store items"); + } + + await db.SaveChangesAsync(ct); + } +} diff --git a/LctMonolith/Database/Models/EventType.cs b/LctMonolith/Database/Models/EventType.cs index f76cdba..1322dd7 100644 --- a/LctMonolith/Database/Models/EventType.cs +++ b/LctMonolith/Database/Models/EventType.cs @@ -1,19 +1,19 @@ -namespace LctMonolith.Models.Database; - -public enum EventType -{ - SkillProgress = 1, - MissionStatusChanged = 2, - RankChanged = 3, - ItemPurchased = 4, - ArtifactObtained = 5, - RewardGranted = 6, - ProfileChanged = 7, - AuthCredentialsChanged = 8, - ItemReturned = 9, - ItemSold = 10 -} - -#if false -// Moved to Models/EventType.cs -#endif +namespace LctMonolith.Models.Database; + +public enum EventType +{ + SkillProgress = 1, + MissionStatusChanged = 2, + RankChanged = 3, + ItemPurchased = 4, + ArtifactObtained = 5, + RewardGranted = 6, + ProfileChanged = 7, + AuthCredentialsChanged = 8, + ItemReturned = 9, + ItemSold = 10 +} + +#if false +// Moved to Models/EventType.cs +#endif diff --git a/LctMonolith/Database/Repositories/GenericRepository.cs b/LctMonolith/Database/Repositories/GenericRepository.cs index 168d593..2614516 100644 --- a/LctMonolith/Database/Repositories/GenericRepository.cs +++ b/LctMonolith/Database/Repositories/GenericRepository.cs @@ -1,56 +1,56 @@ -using System.Linq.Expressions; -using LctMonolith.Database.Data; -using Microsoft.EntityFrameworkCore; - -namespace LctMonolith.Database.Repositories; - -/// -/// Generic repository implementation for common CRUD and query composition. -/// -public class GenericRepository : IGenericRepository where TEntity : class -{ - protected readonly AppDbContext Context; - protected readonly DbSet Set; - - public GenericRepository(AppDbContext context) - { - Context = context; - Set = context.Set(); - } - - public IQueryable Query( - Expression>? filter = null, - Func, IOrderedQueryable>? orderBy = null, - params Expression>[] includes) - { - IQueryable query = Set; - if (filter != null) query = query.Where(filter); - if (includes != null) - { - foreach (var include in includes) - query = query.Include(include); - } - if (orderBy != null) query = orderBy(query); - return query; - } - - public async Task GetByIdAsync(object id) => await Set.FindAsync(id) ?? null; - - public ValueTask FindAsync(params object[] keyValues) => Set.FindAsync(keyValues); - - public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct); - - public async Task AddRangeAsync(IEnumerable entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct); - - public void Update(TEntity entity) => Set.Update(entity); - - public void Remove(TEntity entity) => Set.Remove(entity); - - public async Task RemoveByIdAsync(object id, CancellationToken ct = default) - { - var entity = await Set.FindAsync([id], ct); - if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found"); - Set.Remove(entity); - } -} - +using System.Linq.Expressions; +using LctMonolith.Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace LctMonolith.Database.Repositories; + +/// +/// Generic repository implementation for common CRUD and query composition. +/// +public class GenericRepository : IGenericRepository where TEntity : class +{ + protected readonly AppDbContext Context; + protected readonly DbSet Set; + + public GenericRepository(AppDbContext context) + { + Context = context; + Set = context.Set(); + } + + public IQueryable Query( + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, + params Expression>[] includes) + { + IQueryable query = Set; + if (filter != null) query = query.Where(filter); + if (includes != null) + { + foreach (var include in includes) + query = query.Include(include); + } + if (orderBy != null) query = orderBy(query); + return query; + } + + public async Task GetByIdAsync(object id) => await Set.FindAsync(id) ?? null; + + public ValueTask FindAsync(params object[] keyValues) => Set.FindAsync(keyValues); + + public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct); + + public async Task AddRangeAsync(IEnumerable entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct); + + public void Update(TEntity entity) => Set.Update(entity); + + public void Remove(TEntity entity) => Set.Remove(entity); + + public async Task RemoveByIdAsync(object id, CancellationToken ct = default) + { + var entity = await Set.FindAsync([id], ct); + if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found"); + Set.Remove(entity); + } +} + diff --git a/LctMonolith/Database/Repositories/IGenericRepository.cs b/LctMonolith/Database/Repositories/IGenericRepository.cs index 1d75e37..67152a1 100644 --- a/LctMonolith/Database/Repositories/IGenericRepository.cs +++ b/LctMonolith/Database/Repositories/IGenericRepository.cs @@ -1,25 +1,25 @@ -using System.Linq.Expressions; - -namespace LctMonolith.Database.Repositories; - -/// -/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition. -/// -public interface IGenericRepository where TEntity : class -{ - IQueryable Query( - Expression>? filter = null, - Func, IOrderedQueryable>? orderBy = null, - params Expression>[] includes); - - Task GetByIdAsync(object id); - ValueTask FindAsync(params object[] keyValues); - - Task AddAsync(TEntity entity, CancellationToken ct = default); - Task AddRangeAsync(IEnumerable entities, CancellationToken ct = default); - - void Update(TEntity entity); - void Remove(TEntity entity); - Task RemoveByIdAsync(object id, CancellationToken ct = default); -} - +using System.Linq.Expressions; + +namespace LctMonolith.Database.Repositories; + +/// +/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition. +/// +public interface IGenericRepository where TEntity : class +{ + IQueryable Query( + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, + params Expression>[] includes); + + Task GetByIdAsync(object id); + ValueTask FindAsync(params object[] keyValues); + + Task AddAsync(TEntity entity, CancellationToken ct = default); + Task AddRangeAsync(IEnumerable entities, CancellationToken ct = default); + + void Update(TEntity entity); + void Remove(TEntity entity); + Task RemoveByIdAsync(object id, CancellationToken ct = default); +} + diff --git a/LctMonolith/Database/UnitOfWork/IUnitOfWork.cs b/LctMonolith/Database/UnitOfWork/IUnitOfWork.cs index 657c7ca..e936d28 100644 --- a/LctMonolith/Database/UnitOfWork/IUnitOfWork.cs +++ b/LctMonolith/Database/UnitOfWork/IUnitOfWork.cs @@ -1,40 +1,40 @@ -using LctMonolith.Database.Repositories; -using LctMonolith.Models.Database; -using EventLog = LctMonolith.Models.Database.EventLog; - -namespace LctMonolith.Database.UnitOfWork; - -/// -/// Unit of Work aggregates repositories and transaction boundary. -/// -public interface IUnitOfWork -{ - IGenericRepository Users { get; } - IGenericRepository Players { get; } - IGenericRepository MissionCategories { get; } // added - IGenericRepository Ranks { get; } - IGenericRepository RankMissionRules { get; } - IGenericRepository RankSkillRules { get; } - IGenericRepository Missions { get; } - IGenericRepository PlayerMissions { get; } - IGenericRepository MissionSkillRewards { get; } - IGenericRepository Skills { get; } - IGenericRepository PlayerSkills { get; } - IGenericRepository StoreItems { get; } - IGenericRepository UserInventoryItems { get; } - IGenericRepository Transactions { get; } - IGenericRepository EventLogs { get; } - IGenericRepository RefreshTokens { get; } - IGenericRepository Notifications { get; } - IGenericRepository MissionItemRewards { get; } // added - IGenericRepository MissionRankRules { get; } // added - IGenericRepository Dialogues { get; } - IGenericRepository DialogueMessages { get; } - IGenericRepository DialogueMessageResponseOptions { get; } - IGenericRepository Profiles { get; } - - Task SaveChangesAsync(CancellationToken ct = default); - Task BeginTransactionAsync(CancellationToken ct = default); - Task CommitAsync(CancellationToken ct = default); - Task RollbackAsync(CancellationToken ct = default); -} +using LctMonolith.Database.Repositories; +using LctMonolith.Models.Database; +using EventLog = LctMonolith.Models.Database.EventLog; + +namespace LctMonolith.Database.UnitOfWork; + +/// +/// Unit of Work aggregates repositories and transaction boundary. +/// +public interface IUnitOfWork +{ + IGenericRepository Users { get; } + IGenericRepository Players { get; } + IGenericRepository MissionCategories { get; } // added + IGenericRepository Ranks { get; } + IGenericRepository RankMissionRules { get; } + IGenericRepository RankSkillRules { get; } + IGenericRepository Missions { get; } + IGenericRepository PlayerMissions { get; } + IGenericRepository MissionSkillRewards { get; } + IGenericRepository Skills { get; } + IGenericRepository PlayerSkills { get; } + IGenericRepository StoreItems { get; } + IGenericRepository UserInventoryItems { get; } + IGenericRepository Transactions { get; } + IGenericRepository EventLogs { get; } + IGenericRepository RefreshTokens { get; } + IGenericRepository Notifications { get; } + IGenericRepository MissionItemRewards { get; } // added + IGenericRepository MissionRankRules { get; } // added + IGenericRepository Dialogues { get; } + IGenericRepository DialogueMessages { get; } + IGenericRepository DialogueMessageResponseOptions { get; } + IGenericRepository Profiles { get; } + + Task SaveChangesAsync(CancellationToken ct = default); + Task BeginTransactionAsync(CancellationToken ct = default); + Task CommitAsync(CancellationToken ct = default); + Task RollbackAsync(CancellationToken ct = default); +} diff --git a/LctMonolith/Database/UnitOfWork/UnitOfWork.cs b/LctMonolith/Database/UnitOfWork/UnitOfWork.cs index affcfa9..85e1149 100644 --- a/LctMonolith/Database/UnitOfWork/UnitOfWork.cs +++ b/LctMonolith/Database/UnitOfWork/UnitOfWork.cs @@ -1,110 +1,110 @@ -using LctMonolith.Database.Data; -using LctMonolith.Database.Repositories; -using LctMonolith.Models.Database; -using Microsoft.EntityFrameworkCore.Storage; -using EventLog = LctMonolith.Models.Database.EventLog; - -namespace LctMonolith.Database.UnitOfWork; - -/// -/// Unit of Work implementation encapsulating repositories and DB transaction scope. -/// -public class UnitOfWork : IUnitOfWork, IAsyncDisposable -{ - private readonly AppDbContext _ctx; - private IDbContextTransaction? _tx; - - public UnitOfWork(AppDbContext ctx) - { - _ctx = ctx; - } - - private IGenericRepository? _users; - private IGenericRepository? _ranks; - private IGenericRepository? _rankMissionRules; - private IGenericRepository? _rankSkillRules; - private IGenericRepository? _missions; - private IGenericRepository? _playerMissions; - private IGenericRepository? _missionSkillRewards; - private IGenericRepository? _skills; - private IGenericRepository? _playerSkills; - private IGenericRepository? _storeItems; - private IGenericRepository? _userInventoryItems; - private IGenericRepository? _transactions; - private IGenericRepository? _eventLogs; - private IGenericRepository? _refreshTokens; - private IGenericRepository? _notifications; - private IGenericRepository? _players; - private IGenericRepository? _missionCategories; - private IGenericRepository? _missionItemRewards; - private IGenericRepository? _missionRankRules; - private IGenericRepository? _dialogues; - private IGenericRepository? _dialogueMessages; - private IGenericRepository? _dialogueMessageResponseOptions; - private IGenericRepository? _profiles; - - public IGenericRepository Users => _users ??= new GenericRepository(_ctx); - public IGenericRepository Ranks => _ranks ??= new GenericRepository(_ctx); - public IGenericRepository RankMissionRules => _rankMissionRules ??= new GenericRepository(_ctx); - public IGenericRepository RankSkillRules => _rankSkillRules ??= new GenericRepository(_ctx); - public IGenericRepository Missions => _missions ??= new GenericRepository(_ctx); - public IGenericRepository PlayerMissions => _playerMissions ??= new GenericRepository(_ctx); - public IGenericRepository MissionSkillRewards => _missionSkillRewards ??= new GenericRepository(_ctx); - public IGenericRepository Skills => _skills ??= new GenericRepository(_ctx); - public IGenericRepository PlayerSkills => _playerSkills ??= new GenericRepository(_ctx); - public IGenericRepository StoreItems => _storeItems ??= new GenericRepository(_ctx); - public IGenericRepository UserInventoryItems => _userInventoryItems ??= new GenericRepository(_ctx); - public IGenericRepository Transactions => _transactions ??= new GenericRepository(_ctx); - public IGenericRepository EventLogs => _eventLogs ??= new GenericRepository(_ctx); - public IGenericRepository RefreshTokens => _refreshTokens ??= new GenericRepository(_ctx); - public IGenericRepository Notifications => _notifications ??= new GenericRepository(_ctx); - public IGenericRepository Players => _players ??= new GenericRepository(_ctx); - public IGenericRepository MissionCategories => _missionCategories ??= new GenericRepository(_ctx); - public IGenericRepository MissionItemRewards => _missionItemRewards ??= new GenericRepository(_ctx); - public IGenericRepository MissionRankRules => _missionRankRules ??= new GenericRepository(_ctx); - public IGenericRepository Dialogues => _dialogues ??= new GenericRepository(_ctx); - public IGenericRepository DialogueMessages => _dialogueMessages ??= new GenericRepository(_ctx); - public IGenericRepository DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository(_ctx); - public IGenericRepository Profiles => _profiles ??= new GenericRepository(_ctx); - - public Task SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct); - - public async Task BeginTransactionAsync(CancellationToken ct = default) - { - if (_tx != null) throw new InvalidOperationException("Transaction already started"); - _tx = await _ctx.Database.BeginTransactionAsync(ct); - } - - public async Task CommitAsync(CancellationToken ct = default) - { - if (_tx == null) return; - try - { - await _ctx.SaveChangesAsync(ct); - await _tx.CommitAsync(ct); - } - catch - { - await RollbackAsync(ct); - throw; - } - finally - { - await _tx.DisposeAsync(); - _tx = null; - } - } - - public async Task RollbackAsync(CancellationToken ct = default) - { - if (_tx == null) return; - await _tx.RollbackAsync(ct); - await _tx.DisposeAsync(); - _tx = null; - } - - public async ValueTask DisposeAsync() - { - if (_tx != null) await _tx.DisposeAsync(); - } -} +using LctMonolith.Database.Data; +using LctMonolith.Database.Repositories; +using LctMonolith.Models.Database; +using Microsoft.EntityFrameworkCore.Storage; +using EventLog = LctMonolith.Models.Database.EventLog; + +namespace LctMonolith.Database.UnitOfWork; + +/// +/// Unit of Work implementation encapsulating repositories and DB transaction scope. +/// +public class UnitOfWork : IUnitOfWork, IAsyncDisposable +{ + private readonly AppDbContext _ctx; + private IDbContextTransaction? _tx; + + public UnitOfWork(AppDbContext ctx) + { + _ctx = ctx; + } + + private IGenericRepository? _users; + private IGenericRepository? _ranks; + private IGenericRepository? _rankMissionRules; + private IGenericRepository? _rankSkillRules; + private IGenericRepository? _missions; + private IGenericRepository? _playerMissions; + private IGenericRepository? _missionSkillRewards; + private IGenericRepository? _skills; + private IGenericRepository? _playerSkills; + private IGenericRepository? _storeItems; + private IGenericRepository? _userInventoryItems; + private IGenericRepository? _transactions; + private IGenericRepository? _eventLogs; + private IGenericRepository? _refreshTokens; + private IGenericRepository? _notifications; + private IGenericRepository? _players; + private IGenericRepository? _missionCategories; + private IGenericRepository? _missionItemRewards; + private IGenericRepository? _missionRankRules; + private IGenericRepository? _dialogues; + private IGenericRepository? _dialogueMessages; + private IGenericRepository? _dialogueMessageResponseOptions; + private IGenericRepository? _profiles; + + public IGenericRepository Users => _users ??= new GenericRepository(_ctx); + public IGenericRepository Ranks => _ranks ??= new GenericRepository(_ctx); + public IGenericRepository RankMissionRules => _rankMissionRules ??= new GenericRepository(_ctx); + public IGenericRepository RankSkillRules => _rankSkillRules ??= new GenericRepository(_ctx); + public IGenericRepository Missions => _missions ??= new GenericRepository(_ctx); + public IGenericRepository PlayerMissions => _playerMissions ??= new GenericRepository(_ctx); + public IGenericRepository MissionSkillRewards => _missionSkillRewards ??= new GenericRepository(_ctx); + public IGenericRepository Skills => _skills ??= new GenericRepository(_ctx); + public IGenericRepository PlayerSkills => _playerSkills ??= new GenericRepository(_ctx); + public IGenericRepository StoreItems => _storeItems ??= new GenericRepository(_ctx); + public IGenericRepository UserInventoryItems => _userInventoryItems ??= new GenericRepository(_ctx); + public IGenericRepository Transactions => _transactions ??= new GenericRepository(_ctx); + public IGenericRepository EventLogs => _eventLogs ??= new GenericRepository(_ctx); + public IGenericRepository RefreshTokens => _refreshTokens ??= new GenericRepository(_ctx); + public IGenericRepository Notifications => _notifications ??= new GenericRepository(_ctx); + public IGenericRepository Players => _players ??= new GenericRepository(_ctx); + public IGenericRepository MissionCategories => _missionCategories ??= new GenericRepository(_ctx); + public IGenericRepository MissionItemRewards => _missionItemRewards ??= new GenericRepository(_ctx); + public IGenericRepository MissionRankRules => _missionRankRules ??= new GenericRepository(_ctx); + public IGenericRepository Dialogues => _dialogues ??= new GenericRepository(_ctx); + public IGenericRepository DialogueMessages => _dialogueMessages ??= new GenericRepository(_ctx); + public IGenericRepository DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository(_ctx); + public IGenericRepository Profiles => _profiles ??= new GenericRepository(_ctx); + + public Task SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct); + + public async Task BeginTransactionAsync(CancellationToken ct = default) + { + if (_tx != null) throw new InvalidOperationException("Transaction already started"); + _tx = await _ctx.Database.BeginTransactionAsync(ct); + } + + public async Task CommitAsync(CancellationToken ct = default) + { + if (_tx == null) return; + try + { + await _ctx.SaveChangesAsync(ct); + await _tx.CommitAsync(ct); + } + catch + { + await RollbackAsync(ct); + throw; + } + finally + { + await _tx.DisposeAsync(); + _tx = null; + } + } + + public async Task RollbackAsync(CancellationToken ct = default) + { + if (_tx == null) return; + await _tx.RollbackAsync(ct); + await _tx.DisposeAsync(); + _tx = null; + } + + public async ValueTask DisposeAsync() + { + if (_tx != null) await _tx.DisposeAsync(); + } +} diff --git a/LctMonolith/Dockerfile b/LctMonolith/Dockerfile index 583c741..fc5ed8b 100644 --- a/LctMonolith/Dockerfile +++ b/LctMonolith/Dockerfile @@ -1,23 +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 ["LctMonolith/LctMonolith.csproj", "LctMonolith/"] -RUN dotnet restore "LctMonolith/LctMonolith.csproj" -COPY . . -WORKDIR "/src/LctMonolith" -RUN dotnet build "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "LctMonolith.dll"] +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 ["LctMonolith/LctMonolith.csproj", "LctMonolith/"] +RUN dotnet restore "LctMonolith/LctMonolith.csproj" +COPY . . +WORKDIR "/src/LctMonolith" +RUN dotnet build "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./LctMonolith.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "LctMonolith.dll"] diff --git a/LctMonolith/LctMonolith.csproj b/LctMonolith/LctMonolith.csproj index 2227e67..db9d51b 100644 --- a/LctMonolith/LctMonolith.csproj +++ b/LctMonolith/LctMonolith.csproj @@ -1,43 +1,43 @@ - - - - net9.0 - enable - enable - Linux - true - 1591 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .dockerignore - - - - + + + + net9.0 + enable + enable + Linux + true + 1591 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + .dockerignore + + + + diff --git a/LctMonolith/LctMonolith.http b/LctMonolith/LctMonolith.http index ab5fd18..0136c0b 100644 --- a/LctMonolith/LctMonolith.http +++ b/LctMonolith/LctMonolith.http @@ -1,6 +1,6 @@ -@LctMonolith_HostAddress = http://localhost:5217 - -GET {{LctMonolith_HostAddress}}/weatherforecast/ -Accept: application/json - -### +@LctMonolith_HostAddress = http://localhost:5217 + +GET {{LctMonolith_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/LctMonolith/Models/DTO/CreateMissionCategoryDto.cs b/LctMonolith/Models/DTO/CreateMissionCategoryDto.cs index 90bbf23..ca9156c 100644 --- a/LctMonolith/Models/DTO/CreateMissionCategoryDto.cs +++ b/LctMonolith/Models/DTO/CreateMissionCategoryDto.cs @@ -1,3 +1,3 @@ -namespace LctMonolith.Models.DTO; -public class CreateMissionCategoryDto { public string Title { get; set; } = string.Empty; } - +namespace LctMonolith.Models.DTO; +public class CreateMissionCategoryDto { public string Title { get; set; } = string.Empty; } + diff --git a/LctMonolith/Models/DTO/CreateMissionDto.cs b/LctMonolith/Models/DTO/CreateMissionDto.cs index b016e5f..bb0f72b 100644 --- a/LctMonolith/Models/DTO/CreateMissionDto.cs +++ b/LctMonolith/Models/DTO/CreateMissionDto.cs @@ -1,12 +1,12 @@ -namespace LctMonolith.Models.DTO; - -public class CreateMissionDto -{ - public string Title { get; set; } = string.Empty; - public string? Description { get; set; } - public Guid MissionCategoryId { get; set; } - public Guid? ParentMissionId { get; set; } - public int ExpReward { get; set; } - public int ManaReward { get; set; } -} - +namespace LctMonolith.Models.DTO; + +public class CreateMissionDto +{ + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public Guid MissionCategoryId { get; set; } + public Guid? ParentMissionId { get; set; } + public int ExpReward { get; set; } + public int ManaReward { get; set; } +} + diff --git a/LctMonolith/Models/DTO/CreateRankDto.cs b/LctMonolith/Models/DTO/CreateRankDto.cs index 4ce35ab..6d80801 100644 --- a/LctMonolith/Models/DTO/CreateRankDto.cs +++ b/LctMonolith/Models/DTO/CreateRankDto.cs @@ -1,3 +1,3 @@ -namespace LctMonolith.Models.DTO; -public class CreateRankDto { public string Title { get; set; } = string.Empty; public int ExpNeeded { get; set; } } - +namespace LctMonolith.Models.DTO; +public class CreateRankDto { public string Title { get; set; } = string.Empty; public int ExpNeeded { get; set; } } + diff --git a/LctMonolith/Models/DTO/CreateSkillDto.cs b/LctMonolith/Models/DTO/CreateSkillDto.cs index 43d7edb..c8722b9 100644 --- a/LctMonolith/Models/DTO/CreateSkillDto.cs +++ b/LctMonolith/Models/DTO/CreateSkillDto.cs @@ -1,3 +1,3 @@ -namespace LctMonolith.Models.DTO; -public class CreateSkillDto { public string Title { get; set; } = string.Empty; } - +namespace LctMonolith.Models.DTO; +public class CreateSkillDto { public string Title { get; set; } = string.Empty; } + diff --git a/LctMonolith/Models/DTO/MissionCompletionResult.cs b/LctMonolith/Models/DTO/MissionCompletionResult.cs index 2492a12..c6385bd 100644 --- a/LctMonolith/Models/DTO/MissionCompletionResult.cs +++ b/LctMonolith/Models/DTO/MissionCompletionResult.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.DTO; - -public class MissionCompletionResult -{ - public bool Success { get; set; } - public string Message { get; set; } = string.Empty; - public int ExperienceGained { get; set; } - public int ManaGained { get; set; } - public List SkillsProgress { get; set; } = new(); - public List UnlockedMissions { get; set; } = new(); -} +namespace LctMonolith.Models.DTO; + +public class MissionCompletionResult +{ + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public int ExperienceGained { get; set; } + public int ManaGained { get; set; } + public List SkillsProgress { get; set; } = new(); + public List UnlockedMissions { get; set; } = new(); +} diff --git a/LctMonolith/Models/DTO/MissionValidationResult.cs b/LctMonolith/Models/DTO/MissionValidationResult.cs index a33dc76..bb2e07c 100644 --- a/LctMonolith/Models/DTO/MissionValidationResult.cs +++ b/LctMonolith/Models/DTO/MissionValidationResult.cs @@ -1,8 +1,8 @@ -namespace LctMonolith.Models.DTO; - -public class MissionValidationResult -{ - public bool IsValid { get; set; } - public string Message { get; set; } = string.Empty; - public int? SuggestedExperience { get; set; } -} +namespace LctMonolith.Models.DTO; + +public class MissionValidationResult +{ + public bool IsValid { get; set; } + public string Message { get; set; } = string.Empty; + public int? SuggestedExperience { get; set; } +} diff --git a/LctMonolith/Models/DTO/PlayerProgress.cs b/LctMonolith/Models/DTO/PlayerProgress.cs index a4e6750..445dd17 100644 --- a/LctMonolith/Models/DTO/PlayerProgress.cs +++ b/LctMonolith/Models/DTO/PlayerProgress.cs @@ -1,15 +1,15 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Models.DTO; - -public class PlayerProgress -{ - public Guid PlayerId { get; set; } - public string PlayerName { get; set; } = string.Empty; - public Rank? CurrentRank { get; set; } - public int TotalExperience { get; set; } - public int TotalMana { get; set; } - public int CompletedMissions { get; set; } - public int TotalAvailableMissions { get; set; } - public Dictionary SkillLevels { get; set; } = new(); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Models.DTO; + +public class PlayerProgress +{ + public Guid PlayerId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public Rank? CurrentRank { get; set; } + public int TotalExperience { get; set; } + public int TotalMana { get; set; } + public int CompletedMissions { get; set; } + public int TotalAvailableMissions { get; set; } + public Dictionary SkillLevels { get; set; } = new(); +} diff --git a/LctMonolith/Models/DTO/SkillProgress.cs b/LctMonolith/Models/DTO/SkillProgress.cs index d54693f..f06f5cc 100644 --- a/LctMonolith/Models/DTO/SkillProgress.cs +++ b/LctMonolith/Models/DTO/SkillProgress.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.Models.DTO; - -public class SkillProgress -{ - public Guid SkillId { get; set; } - public string SkillTitle { get; set; } = string.Empty; - public int PreviousLevel { get; set; } - public int NewLevel { get; set; } -} +namespace LctMonolith.Models.DTO; + +public class SkillProgress +{ + public Guid SkillId { get; set; } + public string SkillTitle { get; set; } = string.Empty; + public int PreviousLevel { get; set; } + public int NewLevel { get; set; } +} diff --git a/LctMonolith/Models/Database/AppUser.cs b/LctMonolith/Models/Database/AppUser.cs index e343d02..e15bdb0 100644 --- a/LctMonolith/Models/Database/AppUser.cs +++ b/LctMonolith/Models/Database/AppUser.cs @@ -1,23 +1,23 @@ -using Microsoft.AspNetCore.Identity; - -namespace LctMonolith.Models.Database; - -public class AppUser : IdentityUser -{ - public string? FirstName { get; set; } - public string? LastName { get; set; } - public DateOnly? BirthDate { get; set; } - public int Experience { get; set; } - public int Mana { get; set; } - public Guid? RankId { get; set; } - public Rank? Rank { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - public ICollection Competencies { get; set; } = new List(); - public ICollection Missions { get; set; } = new List(); - public ICollection Inventory { get; set; } = new List(); - public ICollection Transactions { get; set; } = new List(); - public ICollection RefreshTokens { get; set; } = new List(); - public ICollection Events { get; set; } = new List(); - public ICollection Notifications { get; set; } = new List(); -} +using Microsoft.AspNetCore.Identity; + +namespace LctMonolith.Models.Database; + +public class AppUser : IdentityUser +{ + public string? FirstName { get; set; } + public string? LastName { get; set; } + public DateOnly? BirthDate { get; set; } + public int Experience { get; set; } + public int Mana { get; set; } + public Guid? RankId { get; set; } + public Rank? Rank { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public ICollection Competencies { get; set; } = new List(); + public ICollection Missions { get; set; } = new List(); + public ICollection Inventory { get; set; } = new List(); + public ICollection Transactions { get; set; } = new List(); + public ICollection RefreshTokens { get; set; } = new List(); + public ICollection Events { get; set; } = new List(); + public ICollection Notifications { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/AuditableEntity.cs b/LctMonolith/Models/Database/AuditableEntity.cs index 3c0337a..8e20f45 100644 --- a/LctMonolith/Models/Database/AuditableEntity.cs +++ b/LctMonolith/Models/Database/AuditableEntity.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.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; -} +namespace LctMonolith.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/LctMonolith/Models/Database/Dialogue.cs b/LctMonolith/Models/Database/Dialogue.cs index 5ac4d20..d51fe59 100644 --- a/LctMonolith/Models/Database/Dialogue.cs +++ b/LctMonolith/Models/Database/Dialogue.cs @@ -1,16 +1,16 @@ -namespace LctMonolith.Models.Database; - -public class Dialogue -{ - public Guid Id { get; set; } - - public Guid MissionId { get; set; } - public required Mission Mission { get; set; } - - public Guid InitialDialogueMessageId { get; set; } - public Dialogue? InitialDialogueMessage { get; set; } - public Guid InterimDialogueMessageId { get; set; } - public Dialogue? InterimDialogueMessage { get; set; } - public Guid EndDialogueMessageId { get; set; } - public Dialogue? EndDialogueMessage { get; set; } -} +namespace LctMonolith.Models.Database; + +public class Dialogue +{ + public Guid Id { get; set; } + + public Guid MissionId { get; set; } + public required Mission Mission { get; set; } + + public Guid InitialDialogueMessageId { get; set; } + public Dialogue? InitialDialogueMessage { get; set; } + public Guid InterimDialogueMessageId { get; set; } + public Dialogue? InterimDialogueMessage { get; set; } + public Guid EndDialogueMessageId { get; set; } + public Dialogue? EndDialogueMessage { get; set; } +} diff --git a/LctMonolith/Models/Database/DialogueMessage.cs b/LctMonolith/Models/Database/DialogueMessage.cs index 6d2ca32..66d138e 100644 --- a/LctMonolith/Models/Database/DialogueMessage.cs +++ b/LctMonolith/Models/Database/DialogueMessage.cs @@ -1,27 +1,27 @@ -using LctMonolith.Models.Enums; - -namespace LctMonolith.Models.Database; - -public class DialogueMessage -{ - public Guid Id { get; set; } - public Character CharacterLeft { get; set; } = Character.None; - public Character CharacterRight { get; set; } = Character.None; - public CharacterAnimation CharacterLeftAnim { get; set; } = CharacterAnimation.Neutral; - public CharacterAnimation CharacterRightAnim { get; set; } = CharacterAnimation.Neutral; - public string CharacterLeftMessage { get; set; } = string.Empty; - public string CharacterRightMessage { get; set; } = string.Empty; - public MessageStyle CharacterLeftMessageStyle { get; set; } = MessageStyle.Normal; - public MessageStyle CharacterRightMessageStyle { get; set; } = MessageStyle.Normal; - public bool AllowMessageAi { get; set; } - public string MessageAiButtonText { get; set; } = string.Empty; - - public Guid InitialDialogueId { get; set; } - public Dialogue? InitialDialogue { get; set; } - public Guid InterimDialogueId { get; set; } - public Dialogue? InterimDialogue { get; set; } - public Guid EndDialogueId { get; set; } - public Dialogue? EndDialogue { get; set; } - - public ICollection DialogueMessageResponseOptions = new List(); -} +using LctMonolith.Models.Enums; + +namespace LctMonolith.Models.Database; + +public class DialogueMessage +{ + public Guid Id { get; set; } + public Character CharacterLeft { get; set; } = Character.None; + public Character CharacterRight { get; set; } = Character.None; + public CharacterAnimation CharacterLeftAnim { get; set; } = CharacterAnimation.Neutral; + public CharacterAnimation CharacterRightAnim { get; set; } = CharacterAnimation.Neutral; + public string CharacterLeftMessage { get; set; } = string.Empty; + public string CharacterRightMessage { get; set; } = string.Empty; + public MessageStyle CharacterLeftMessageStyle { get; set; } = MessageStyle.Normal; + public MessageStyle CharacterRightMessageStyle { get; set; } = MessageStyle.Normal; + public bool AllowMessageAi { get; set; } + public string MessageAiButtonText { get; set; } = string.Empty; + + public Guid InitialDialogueId { get; set; } + public Dialogue? InitialDialogue { get; set; } + public Guid InterimDialogueId { get; set; } + public Dialogue? InterimDialogue { get; set; } + public Guid EndDialogueId { get; set; } + public Dialogue? EndDialogue { get; set; } + + public ICollection DialogueMessageResponseOptions = new List(); +} diff --git a/LctMonolith/Models/Database/DialogueMessageResponseOption.cs b/LctMonolith/Models/Database/DialogueMessageResponseOption.cs index 9912921..714fc14 100644 --- a/LctMonolith/Models/Database/DialogueMessageResponseOption.cs +++ b/LctMonolith/Models/Database/DialogueMessageResponseOption.cs @@ -1,16 +1,16 @@ -using LctMonolith.Models.Enums; - -namespace LctMonolith.Models.Database; - -public class DialogueMessageResponseOption -{ - public Guid Id { get; set; } - public string Message { get; set; } = "..."; - public MessageStyle MessageStyle { get; set; } = MessageStyle.Normal; - public int z { get; set; } - - public Guid ParentDialogueMessageId { get; set; } - public required DialogueMessage ParentDialogueMessage { get; set; } - public Guid DestinationDialogueMessageId { get; set; } - public DialogueMessage? DestinationDialogueMessage { get; set; } -} +using LctMonolith.Models.Enums; + +namespace LctMonolith.Models.Database; + +public class DialogueMessageResponseOption +{ + public Guid Id { get; set; } + public string Message { get; set; } = "..."; + public MessageStyle MessageStyle { get; set; } = MessageStyle.Normal; + public int z { get; set; } + + public Guid ParentDialogueMessageId { get; set; } + public required DialogueMessage ParentDialogueMessage { get; set; } + public Guid DestinationDialogueMessageId { get; set; } + public DialogueMessage? DestinationDialogueMessage { get; set; } +} diff --git a/LctMonolith/Models/Database/EventLog.cs b/LctMonolith/Models/Database/EventLog.cs index 90974f0..57a58c5 100644 --- a/LctMonolith/Models/Database/EventLog.cs +++ b/LctMonolith/Models/Database/EventLog.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.Database; - -public class EventLog -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public EventType Type { get; set; } - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - public string? Data { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} +namespace LctMonolith.Models.Database; + +public class EventLog +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public EventType Type { get; set; } + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + public string? Data { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/LctMonolith/Models/Database/Mission.cs b/LctMonolith/Models/Database/Mission.cs index 47997dd..461b9ce 100644 --- a/LctMonolith/Models/Database/Mission.cs +++ b/LctMonolith/Models/Database/Mission.cs @@ -1,23 +1,23 @@ -namespace LctMonolith.Models.Database; - -public class Mission -{ - public Guid Id { get; set; } - public string Title { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public MissionCategory? MissionCategory { get; set; } - public Guid MissionCategoryId { get; set; } // changed from long - public Mission? ParentMission { get; set; } - public Guid? ParentMissionId { get; set; } // changed from long to nullable Guid - public int ExpReward { get; set; } - public int ManaReward { get; set; } - public Guid DialogueId { get; set; } - public Dialogue? Dialogue { get; set; } - - public ICollection ChildMissions { get; set; } = new List(); - public ICollection PlayerMissions { get; set; } = new List(); - public ICollection MissionItemRewards { get; set; } = new List(); - public ICollection MissionSkillRewards { get; set; } = new List(); - public ICollection MissionRankRules { get; set; } = new List(); - public ICollection RankMissionRules { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class Mission +{ + public Guid Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public MissionCategory? MissionCategory { get; set; } + public Guid MissionCategoryId { get; set; } // changed from long + public Mission? ParentMission { get; set; } + public Guid? ParentMissionId { get; set; } // changed from long to nullable Guid + public int ExpReward { get; set; } + public int ManaReward { get; set; } + public Guid DialogueId { get; set; } + public Dialogue? Dialogue { get; set; } + + public ICollection ChildMissions { get; set; } = new List(); + public ICollection PlayerMissions { get; set; } = new List(); + public ICollection MissionItemRewards { get; set; } = new List(); + public ICollection MissionSkillRewards { get; set; } = new List(); + public ICollection MissionRankRules { get; set; } = new List(); + public ICollection RankMissionRules { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/MissionCategory.cs b/LctMonolith/Models/Database/MissionCategory.cs index 7d6cc64..f000630 100644 --- a/LctMonolith/Models/Database/MissionCategory.cs +++ b/LctMonolith/Models/Database/MissionCategory.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.Models.Database; - -public class MissionCategory -{ - public Guid Id { get; set; } - public string Title { get; set; } = string.Empty; - - public ICollection Missions { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class MissionCategory +{ + public Guid Id { get; set; } + public string Title { get; set; } = string.Empty; + + public ICollection Missions { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/MissionItemReward.cs b/LctMonolith/Models/Database/MissionItemReward.cs index 56ecada..b1b57eb 100644 --- a/LctMonolith/Models/Database/MissionItemReward.cs +++ b/LctMonolith/Models/Database/MissionItemReward.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.Models.Database; - -public class MissionItemReward -{ - public Guid Id { get; set; } - public Guid ItemId { get; set; } - public Guid MissionId { get; set; } - public required Mission Mission { get; set; } -} +namespace LctMonolith.Models.Database; + +public class MissionItemReward +{ + public Guid Id { get; set; } + public Guid ItemId { get; set; } + public Guid MissionId { get; set; } + public required Mission Mission { get; set; } +} diff --git a/LctMonolith/Models/Database/MissionRankRule.cs b/LctMonolith/Models/Database/MissionRankRule.cs index 7c5d3bf..5688225 100644 --- a/LctMonolith/Models/Database/MissionRankRule.cs +++ b/LctMonolith/Models/Database/MissionRankRule.cs @@ -1,10 +1,10 @@ -namespace LctMonolith.Models.Database; - -public class MissionRankRule -{ - public Guid Id { get; set; } - public Guid MissionId { get; set; } - public Mission Mission { get; set; } = null!; - public Guid RankId { get; set; } - public Rank Rank { get; set; } = null!; -} +namespace LctMonolith.Models.Database; + +public class MissionRankRule +{ + public Guid Id { get; set; } + public Guid MissionId { get; set; } + public Mission Mission { get; set; } = null!; + public Guid RankId { get; set; } + public Rank Rank { get; set; } = null!; +} diff --git a/LctMonolith/Models/Database/MissionSkillReward.cs b/LctMonolith/Models/Database/MissionSkillReward.cs index d09fa66..49a9b3f 100644 --- a/LctMonolith/Models/Database/MissionSkillReward.cs +++ b/LctMonolith/Models/Database/MissionSkillReward.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.Database; - -public class MissionSkillReward -{ - public Guid Id { get; set; } - public Guid MissionId { get; set; } - public Mission Mission { get; set; } = null!; - public Guid SkillId { get; set; } // changed from long - public Skill Skill { get; set; } = null!; - public int Value { get; set; } -} +namespace LctMonolith.Models.Database; + +public class MissionSkillReward +{ + public Guid Id { get; set; } + public Guid MissionId { get; set; } + public Mission Mission { get; set; } = null!; + public Guid SkillId { get; set; } // changed from long + public Skill Skill { get; set; } = null!; + public int Value { get; set; } +} diff --git a/LctMonolith/Models/Database/Notification.cs b/LctMonolith/Models/Database/Notification.cs index abcfb62..c4b54d3 100644 --- a/LctMonolith/Models/Database/Notification.cs +++ b/LctMonolith/Models/Database/Notification.cs @@ -1,14 +1,14 @@ -namespace LctMonolith.Models.Database; - -public class Notification -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - public string Type { get; set; } = null!; - public string Title { get; set; } = null!; - public string Message { get; set; } = null!; - public bool IsRead { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime? ReadAt { get; set; } -} +namespace LctMonolith.Models.Database; + +public class Notification +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + public string Type { get; set; } = null!; + public string Title { get; set; } = null!; + public string Message { get; set; } = null!; + public bool IsRead { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? ReadAt { get; set; } +} diff --git a/LctMonolith/Models/Database/Player.cs b/LctMonolith/Models/Database/Player.cs index 0ee1744..e44c8c4 100644 --- a/LctMonolith/Models/Database/Player.cs +++ b/LctMonolith/Models/Database/Player.cs @@ -1,14 +1,14 @@ -namespace LctMonolith.Models.Database; - -public class Player -{ - public Guid Id { get; set; } - public Guid UserId { get; set; } // 1:1 to AppUser (retain linkage) - public Guid RankId { get; set; } - public Rank? Rank { get; set; } - public int Experience { get; set; } - public int Mana { get; set; } - - public ICollection PlayerMissions { get; set; } = new List(); - public ICollection PlayerSkills { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class Player +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } // 1:1 to AppUser (retain linkage) + public Guid RankId { get; set; } + public Rank? Rank { get; set; } + public int Experience { get; set; } + public int Mana { get; set; } + + public ICollection PlayerMissions { get; set; } = new List(); + public ICollection PlayerSkills { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/PlayerMission.cs b/LctMonolith/Models/Database/PlayerMission.cs index c153416..560097e 100644 --- a/LctMonolith/Models/Database/PlayerMission.cs +++ b/LctMonolith/Models/Database/PlayerMission.cs @@ -1,14 +1,14 @@ -namespace LctMonolith.Models.Database; - -public class PlayerMission -{ - public Guid Id { get; set; } - public Guid PlayerId { get; set; } - public Player Player { get; set; } = null!; // removed required - public Guid MissionId { get; set; } - public Mission Mission { get; set; } = null!; // removed required - public DateTime? Started { get; set; } - public DateTime? Completed { get; set; } - public DateTime? RewardsRedeemed { get; set; } - public int ProgressPercent { get; set; } // 0..100 -} +namespace LctMonolith.Models.Database; + +public class PlayerMission +{ + public Guid Id { get; set; } + public Guid PlayerId { get; set; } + public Player Player { get; set; } = null!; // removed required + public Guid MissionId { get; set; } + public Mission Mission { get; set; } = null!; // removed required + public DateTime? Started { get; set; } + public DateTime? Completed { get; set; } + public DateTime? RewardsRedeemed { get; set; } + public int ProgressPercent { get; set; } // 0..100 +} diff --git a/LctMonolith/Models/Database/PlayerSkill.cs b/LctMonolith/Models/Database/PlayerSkill.cs index b7b434f..e0fcce1 100644 --- a/LctMonolith/Models/Database/PlayerSkill.cs +++ b/LctMonolith/Models/Database/PlayerSkill.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.Database; - -public class PlayerSkill -{ - public Guid Id { get; set; } - public Guid PlayerId { get; set; } - public Player Player { get; set; } = null!; - public Guid SkillId { get; set; } - public Skill Skill { get; set; } = null!; - public int Score { get; set; } -} +namespace LctMonolith.Models.Database; + +public class PlayerSkill +{ + public Guid Id { get; set; } + public Guid PlayerId { get; set; } + public Player Player { get; set; } = null!; + public Guid SkillId { get; set; } + public Skill Skill { get; set; } = null!; + public int Score { get; set; } +} diff --git a/LctMonolith/Models/Database/Profile.cs b/LctMonolith/Models/Database/Profile.cs index cc82d98..809810e 100644 --- a/LctMonolith/Models/Database/Profile.cs +++ b/LctMonolith/Models/Database/Profile.cs @@ -1,21 +1,21 @@ -namespace LctMonolith.Models.Database; - -public class Profile -{ - public Guid Id { get; set; } - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - - public string? FirstName { get; set; } - public string? LastName { get; set; } - public DateOnly? BirthDate { get; set; } - public string? About { get; set; } - public string? Location { get; set; } - - // Avatar in S3 / MinIO - public string? AvatarS3Key { get; set; } - public string? AvatarUrl { get; set; } - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; -} +namespace LctMonolith.Models.Database; + +public class Profile +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + + public string? FirstName { get; set; } + public string? LastName { get; set; } + public DateOnly? BirthDate { get; set; } + public string? About { get; set; } + public string? Location { get; set; } + + // Avatar in S3 / MinIO + public string? AvatarS3Key { get; set; } + public string? AvatarUrl { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/LctMonolith/Models/Database/Rank.cs b/LctMonolith/Models/Database/Rank.cs index ec9ed7e..d85b888 100644 --- a/LctMonolith/Models/Database/Rank.cs +++ b/LctMonolith/Models/Database/Rank.cs @@ -1,13 +1,13 @@ -namespace LctMonolith.Models.Database; - -public class Rank -{ - public Guid Id { get; set; } - public string Title { get; set; } = string.Empty; - public int ExpNeeded { get; set; } - - public ICollection Players { get; set; } = new List(); - public ICollection MissionRankRules { get; set; } = new List(); - public ICollection RankMissionRules { get; set; } = new List(); - public ICollection RankSkillRules { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class Rank +{ + public Guid Id { get; set; } + public string Title { get; set; } = string.Empty; + public int ExpNeeded { get; set; } + + public ICollection Players { get; set; } = new List(); + public ICollection MissionRankRules { get; set; } = new List(); + public ICollection RankMissionRules { get; set; } = new List(); + public ICollection RankSkillRules { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/RankMissionRule.cs b/LctMonolith/Models/Database/RankMissionRule.cs index ee60e54..12ff408 100644 --- a/LctMonolith/Models/Database/RankMissionRule.cs +++ b/LctMonolith/Models/Database/RankMissionRule.cs @@ -1,10 +1,10 @@ -namespace LctMonolith.Models.Database; - -public class RankMissionRule -{ - public Guid Id { get; set; } - public Guid RankId { get; set; } - public Rank Rank { get; set; } = null!; - public Guid MissionId { get; set; } - public Mission Mission { get; set; } = null!; -} +namespace LctMonolith.Models.Database; + +public class RankMissionRule +{ + public Guid Id { get; set; } + public Guid RankId { get; set; } + public Rank Rank { get; set; } = null!; + public Guid MissionId { get; set; } + public Mission Mission { get; set; } = null!; +} diff --git a/LctMonolith/Models/Database/RankSkillRule.cs b/LctMonolith/Models/Database/RankSkillRule.cs index fdf5cfc..ade19e5 100644 --- a/LctMonolith/Models/Database/RankSkillRule.cs +++ b/LctMonolith/Models/Database/RankSkillRule.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.Database; - -public class RankSkillRule -{ - public Guid Id { get; set; } - public Guid RankId { get; set; } - public Rank Rank { get; set; } = null!; - public Guid SkillId { get; set; } - public Skill Skill { get; set; } = null!; - public int Min { get; set; } -} +namespace LctMonolith.Models.Database; + +public class RankSkillRule +{ + public Guid Id { get; set; } + public Guid RankId { get; set; } + public Rank Rank { get; set; } = null!; + public Guid SkillId { get; set; } + public Skill Skill { get; set; } = null!; + public int Min { get; set; } +} diff --git a/LctMonolith/Models/Database/RefreshToken.cs b/LctMonolith/Models/Database/RefreshToken.cs index 46f0972..94626c1 100644 --- a/LctMonolith/Models/Database/RefreshToken.cs +++ b/LctMonolith/Models/Database/RefreshToken.cs @@ -1,12 +1,12 @@ -namespace LctMonolith.Models.Database; - -public class RefreshToken -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public string Token { get; set; } = null!; - public DateTime ExpiresAt { get; set; } - public bool IsRevoked { get; set; } - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} +namespace LctMonolith.Models.Database; + +public class RefreshToken +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Token { get; set; } = null!; + public DateTime ExpiresAt { get; set; } + public bool IsRevoked { get; set; } + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/LctMonolith/Models/Database/Skill.cs b/LctMonolith/Models/Database/Skill.cs index 7ea0806..656a644 100644 --- a/LctMonolith/Models/Database/Skill.cs +++ b/LctMonolith/Models/Database/Skill.cs @@ -1,10 +1,10 @@ -namespace LctMonolith.Models.Database; - -public class Skill -{ - public Guid Id { get; set; } - public string Title { get; set; } = string.Empty; - public ICollection MissionSkillRewards { get; set; } = new List(); - public ICollection RankSkillRules { get; set; } = new List(); - public ICollection PlayerSkills { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class Skill +{ + public Guid Id { get; set; } + public string Title { get; set; } = string.Empty; + public ICollection MissionSkillRewards { get; set; } = new List(); + public ICollection RankSkillRules { get; set; } = new List(); + public ICollection PlayerSkills { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/StoreItem.cs b/LctMonolith/Models/Database/StoreItem.cs index f5fcfc8..e3484bc 100644 --- a/LctMonolith/Models/Database/StoreItem.cs +++ b/LctMonolith/Models/Database/StoreItem.cs @@ -1,12 +1,12 @@ -namespace LctMonolith.Models.Database; - -public class StoreItem -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public string Name { get; set; } = null!; - public string? Description { get; set; } - public int Price { get; set; } - public bool IsActive { get; set; } = true; - public int? Stock { get; set; } - public ICollection UserInventory { get; set; } = new List(); -} +namespace LctMonolith.Models.Database; + +public class StoreItem +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = null!; + public string? Description { get; set; } + public int Price { get; set; } + public bool IsActive { get; set; } = true; + public int? Stock { get; set; } + public ICollection UserInventory { get; set; } = new List(); +} diff --git a/LctMonolith/Models/Database/Transaction.cs b/LctMonolith/Models/Database/Transaction.cs index 19d02da..96069ae 100644 --- a/LctMonolith/Models/Database/Transaction.cs +++ b/LctMonolith/Models/Database/Transaction.cs @@ -1,13 +1,13 @@ -namespace LctMonolith.Models.Database; - -public class Transaction -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - public TransactionType Type { get; set; } - public Guid? StoreItemId { get; set; } - public StoreItem? StoreItem { get; set; } - public int ManaAmount { get; set; } - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} +namespace LctMonolith.Models.Database; + +public class Transaction +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + public TransactionType Type { get; set; } + public Guid? StoreItemId { get; set; } + public StoreItem? StoreItem { get; set; } + public int ManaAmount { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/LctMonolith/Models/Database/TransactionType.cs b/LctMonolith/Models/Database/TransactionType.cs index 3a09669..950cbfc 100644 --- a/LctMonolith/Models/Database/TransactionType.cs +++ b/LctMonolith/Models/Database/TransactionType.cs @@ -1,3 +1,3 @@ -namespace LctMonolith.Models.Database; - -public enum TransactionType { Purchase = 0, Return = 1, Sale = 2 } +namespace LctMonolith.Models.Database; + +public enum TransactionType { Purchase = 0, Return = 1, Sale = 2 } diff --git a/LctMonolith/Models/Database/UserInventoryItem.cs b/LctMonolith/Models/Database/UserInventoryItem.cs index 032d8bb..7272ff1 100644 --- a/LctMonolith/Models/Database/UserInventoryItem.cs +++ b/LctMonolith/Models/Database/UserInventoryItem.cs @@ -1,12 +1,12 @@ -namespace LctMonolith.Models.Database; - -public class UserInventoryItem -{ - public Guid UserId { get; set; } - public AppUser User { get; set; } = null!; - public Guid StoreItemId { get; set; } - public StoreItem StoreItem { get; set; } = null!; - public int Quantity { get; set; } = 1; - public DateTime AcquiredAt { get; set; } = DateTime.UtcNow; - public bool IsReturned { get; set; } -} +namespace LctMonolith.Models.Database; + +public class UserInventoryItem +{ + public Guid UserId { get; set; } + public AppUser User { get; set; } = null!; + public Guid StoreItemId { get; set; } + public StoreItem StoreItem { get; set; } = null!; + public int Quantity { get; set; } = 1; + public DateTime AcquiredAt { get; set; } = DateTime.UtcNow; + public bool IsReturned { get; set; } +} diff --git a/LctMonolith/Models/Enums/Character.cs b/LctMonolith/Models/Enums/Character.cs index 5ac7e65..738473c 100644 --- a/LctMonolith/Models/Enums/Character.cs +++ b/LctMonolith/Models/Enums/Character.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Models.Enums; - -public enum Character -{ - None, - Assistant, - Pilot, - Janitor, - Administrator, - Mechanic -} +namespace LctMonolith.Models.Enums; + +public enum Character +{ + None, + Assistant, + Pilot, + Janitor, + Administrator, + Mechanic +} diff --git a/LctMonolith/Models/Enums/CharacterAnimation.cs b/LctMonolith/Models/Enums/CharacterAnimation.cs index f86a95b..03f5fed 100644 --- a/LctMonolith/Models/Enums/CharacterAnimation.cs +++ b/LctMonolith/Models/Enums/CharacterAnimation.cs @@ -1,18 +1,18 @@ -namespace LctMonolith.Models.Enums; - -public enum CharacterAnimation -{ - Neutral, - Happy, - Laughter, - Mock, - Sad, - Crying, - Annoyed, - Angry, - Threats, - Wave, - Silhouette, - Scared, - Embarassed -} +namespace LctMonolith.Models.Enums; + +public enum CharacterAnimation +{ + Neutral, + Happy, + Laughter, + Mock, + Sad, + Crying, + Annoyed, + Angry, + Threats, + Wave, + Silhouette, + Scared, + Embarassed +} diff --git a/LctMonolith/Models/Enums/MessageStyle.cs b/LctMonolith/Models/Enums/MessageStyle.cs index 26b868d..2fdc4fa 100644 --- a/LctMonolith/Models/Enums/MessageStyle.cs +++ b/LctMonolith/Models/Enums/MessageStyle.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.Models.Enums; - -public enum MessageStyle -{ - Normal, - Loud, - Think, - Action -} +namespace LctMonolith.Models.Enums; + +public enum MessageStyle +{ + Normal, + Loud, + Think, + Action +} diff --git a/LctMonolith/Program.cs b/LctMonolith/Program.cs index a96c0d0..d1ff2e6 100644 --- a/LctMonolith/Program.cs +++ b/LctMonolith/Program.cs @@ -1,134 +1,134 @@ -using Serilog; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; -using System.Text; -using LctMonolith.Models.Database; // replaced Domain.Entities -using Microsoft.AspNetCore.Identity; -using LctMonolith.Application.Middleware; -using LctMonolith.Services; -using LctMonolith.Application.Options; -using LctMonolith.Database.Data; -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Services.Contracts; // Added for JwtOptions -using LctMonolith.Application.Extensions; // added - -var builder = WebApplication.CreateBuilder(args); - -// Serilog configuration -builder.Host.UseSerilog((ctx, services, loggerConfig) => - loggerConfig - .ReadFrom.Configuration(ctx.Configuration) - .Enrich.FromLogContext() - .Enrich.WithProperty("Application", "LctMonolith")); - -// Configuration values -var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres"; -var jwtSection = builder.Configuration.GetSection("Jwt"); -var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me"; -var jwtIssuer = jwtSection["Issuer"] ?? "LctMonolith"; -var jwtAudience = jwtSection["Audience"] ?? "LctMonolithAudience"; -var accessMinutes = int.TryParse(jwtSection["AccessTokenMinutes"], out var m) ? m : 60; -var refreshDays = int.TryParse(jwtSection["RefreshTokenDays"], out var d) ? d : 7; -var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)); - -builder.Services.Configure(o => -{ - o.Key = jwtKey; - o.Issuer = jwtIssuer; - o.Audience = jwtAudience; - o.AccessTokenMinutes = accessMinutes; - o.RefreshTokenDays = refreshDays; -}); - -// DbContext -builder.Services.AddDbContext(opt => - opt.UseNpgsql(connectionString)); - -// Identity Core -builder.Services.AddIdentityCore(options => - { - options.Password.RequireDigit = false; - options.Password.RequireUppercase = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequireLowercase = false; - options.Password.RequiredLength = 6; - }) - .AddRoles>() - .AddEntityFrameworkStores() - .AddSignInManager>() - .AddDefaultTokenProviders(); - -// Authentication & JWT -builder.Services.AddAuthentication(o => - { - o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(o => - { - o.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ValidIssuer = jwtIssuer, - ValidAudience = jwtAudience, - IssuerSigningKey = signingKey, - ClockSkew = TimeSpan.FromMinutes(2) - }; - }); - -// Controllers + NewtonsoftJson -builder.Services.AddControllers() - .AddNewtonsoftJson(); - -// OpenAPI -builder.Services.AddOpenApi(); - -// Health checks -builder.Services.AddHealthChecks(); - -// Remove individual service registrations and replace with extension -// builder.Services.AddScoped(); -// builder.Services.AddScoped(); -// builder.Services.AddScoped(); -// builder.Services.AddScoped(); -// builder.Services.AddScoped(); - -builder.Services.AddApplicationServices(builder.Configuration); - -// CORS -builder.Services.AddCors(p => p.AddDefaultPolicy(policy => - policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin())); - -var app = builder.Build(); - -using (var scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); - await DbSeeder.SeedAsync(db); // seed dev data -} - -app.UseSerilogRequestLogging(); - -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} - -app.UseCors(); -app.UseHttpsRedirection(); -app.UseErrorHandling(); -app.UseAuthentication(); -app.UseAuthorization(); - -app.MapControllers(); - -app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); -app.MapHealthChecks("/health/ready"); - -app.Run(); +using Serilog; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using LctMonolith.Models.Database; // replaced Domain.Entities +using Microsoft.AspNetCore.Identity; +using LctMonolith.Application.Middleware; +using LctMonolith.Services; +using LctMonolith.Application.Options; +using LctMonolith.Database.Data; +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Services.Contracts; // Added for JwtOptions +using LctMonolith.Application.Extensions; // added + +var builder = WebApplication.CreateBuilder(args); + +// Serilog configuration +builder.Host.UseSerilog((ctx, services, loggerConfig) => + loggerConfig + .ReadFrom.Configuration(ctx.Configuration) + .Enrich.FromLogContext() + .Enrich.WithProperty("Application", "LctMonolith")); + +// Configuration values +var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres"; +var jwtSection = builder.Configuration.GetSection("Jwt"); +var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me"; +var jwtIssuer = jwtSection["Issuer"] ?? "LctMonolith"; +var jwtAudience = jwtSection["Audience"] ?? "LctMonolithAudience"; +var accessMinutes = int.TryParse(jwtSection["AccessTokenMinutes"], out var m) ? m : 60; +var refreshDays = int.TryParse(jwtSection["RefreshTokenDays"], out var d) ? d : 7; +var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)); + +builder.Services.Configure(o => +{ + o.Key = jwtKey; + o.Issuer = jwtIssuer; + o.Audience = jwtAudience; + o.AccessTokenMinutes = accessMinutes; + o.RefreshTokenDays = refreshDays; +}); + +// DbContext +builder.Services.AddDbContext(opt => + opt.UseNpgsql(connectionString)); + +// Identity Core +builder.Services.AddIdentityCore(options => + { + options.Password.RequireDigit = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireLowercase = false; + options.Password.RequiredLength = 6; + }) + .AddRoles>() + .AddEntityFrameworkStores() + .AddSignInManager>() + .AddDefaultTokenProviders(); + +// Authentication & JWT +builder.Services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(o => + { + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidIssuer = jwtIssuer, + ValidAudience = jwtAudience, + IssuerSigningKey = signingKey, + ClockSkew = TimeSpan.FromMinutes(2) + }; + }); + +// Controllers + NewtonsoftJson +builder.Services.AddControllers() + .AddNewtonsoftJson(); + +// OpenAPI +builder.Services.AddOpenApi(); + +// Health checks +builder.Services.AddHealthChecks(); + +// Remove individual service registrations and replace with extension +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); +// builder.Services.AddScoped(); + +builder.Services.AddApplicationServices(builder.Configuration); + +// CORS +builder.Services.AddCors(p => p.AddDefaultPolicy(policy => + policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin())); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + await DbSeeder.SeedAsync(db); // seed dev data +} + +app.UseSerilogRequestLogging(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseCors(); +app.UseHttpsRedirection(); +app.UseErrorHandling(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); +app.MapHealthChecks("/health/ready"); + +app.Run(); diff --git a/LctMonolith/Properties/launchSettings.json b/LctMonolith/Properties/launchSettings.json index 529ed3a..3d35119 100644 --- a/LctMonolith/Properties/launchSettings.json +++ b/LctMonolith/Properties/launchSettings.json @@ -1,23 +1,23 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "http://localhost:5217", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": false, - "applicationUrl": "https://localhost:7144;http://localhost:5217", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5217", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7144;http://localhost:5217", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LctMonolith/Services/AnalyticsService.cs b/LctMonolith/Services/AnalyticsService.cs index 1cd8a33..40834a2 100644 --- a/LctMonolith/Services/AnalyticsService.cs +++ b/LctMonolith/Services/AnalyticsService.cs @@ -1,41 +1,41 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Models; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -/// -/// Provides aggregated analytics metrics for dashboards. -/// -public class AnalyticsService : IAnalyticsService -{ - private readonly IUnitOfWork _uow; - public AnalyticsService(IUnitOfWork uow) => _uow = uow; - - public async Task GetSummaryAsync(CancellationToken ct = default) - { - try - { - var totalUsers = await _uow.Users.Query().CountAsync(ct); - var totalMissions = await _uow.Missions.Query().CountAsync(ct); - var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct); - var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct); - var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct); - return new AnalyticsSummary - { - TotalUsers = totalUsers, - TotalMissions = totalMissions, - TotalStoreItems = totalStoreItems, - TotalExperience = totalExperience, - CompletedMissions = completedMissions - }; - } - catch (Exception ex) - { - Log.Error(ex, "Failed to build analytics summary"); - throw; - } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Models; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +/// +/// Provides aggregated analytics metrics for dashboards. +/// +public class AnalyticsService : IAnalyticsService +{ + private readonly IUnitOfWork _uow; + public AnalyticsService(IUnitOfWork uow) => _uow = uow; + + public async Task GetSummaryAsync(CancellationToken ct = default) + { + try + { + var totalUsers = await _uow.Users.Query().CountAsync(ct); + var totalMissions = await _uow.Missions.Query().CountAsync(ct); + var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct); + var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct); + var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct); + return new AnalyticsSummary + { + TotalUsers = totalUsers, + TotalMissions = totalMissions, + TotalStoreItems = totalStoreItems, + TotalExperience = totalExperience, + CompletedMissions = completedMissions + }; + } + catch (Exception ex) + { + Log.Error(ex, "Failed to build analytics summary"); + throw; + } + } +} diff --git a/LctMonolith/Services/Contracts/IAnalyticsService.cs b/LctMonolith/Services/Contracts/IAnalyticsService.cs index 1a589f4..c072899 100644 --- a/LctMonolith/Services/Contracts/IAnalyticsService.cs +++ b/LctMonolith/Services/Contracts/IAnalyticsService.cs @@ -1,8 +1,8 @@ -using LctMonolith.Services.Models; - -namespace LctMonolith.Services; - -public interface IAnalyticsService -{ - Task GetSummaryAsync(CancellationToken ct = default); -} +using LctMonolith.Services.Models; + +namespace LctMonolith.Services; + +public interface IAnalyticsService +{ + Task GetSummaryAsync(CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/IDialogueService.cs b/LctMonolith/Services/Contracts/IDialogueService.cs index 0f46d1f..f37f127 100644 --- a/LctMonolith/Services/Contracts/IDialogueService.cs +++ b/LctMonolith/Services/Contracts/IDialogueService.cs @@ -1,12 +1,12 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IDialogueService -{ - Task GetDialogueByMissionIdAsync(Guid missionId); - Task CreateDialogueAsync(Dialogue dialogue); - Task GetDialogueMessageByIdAsync(Guid messageId); - Task> GetResponseOptionsAsync(Guid messageId); - Task ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IDialogueService +{ + Task GetDialogueByMissionIdAsync(Guid missionId); + Task CreateDialogueAsync(Dialogue dialogue); + Task GetDialogueMessageByIdAsync(Guid messageId); + Task> GetResponseOptionsAsync(Guid messageId); + Task ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId); +} diff --git a/LctMonolith/Services/Contracts/IFileStorageService.cs b/LctMonolith/Services/Contracts/IFileStorageService.cs index 3a97e97..79f22bb 100644 --- a/LctMonolith/Services/Contracts/IFileStorageService.cs +++ b/LctMonolith/Services/Contracts/IFileStorageService.cs @@ -1,8 +1,8 @@ -namespace LctMonolith.Services.Interfaces; - -public interface IFileStorageService -{ - Task UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default); - Task DeleteAsync(string key, CancellationToken ct = default); - Task GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default); -} +namespace LctMonolith.Services.Interfaces; + +public interface IFileStorageService +{ + Task UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default); + Task DeleteAsync(string key, CancellationToken ct = default); + Task GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/IInventoryService.cs b/LctMonolith/Services/Contracts/IInventoryService.cs index 760fbc4..1379f77 100644 --- a/LctMonolith/Services/Contracts/IInventoryService.cs +++ b/LctMonolith/Services/Contracts/IInventoryService.cs @@ -1,8 +1,8 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Contracts; - -public interface IInventoryService -{ - Task> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Contracts; + +public interface IInventoryService +{ + Task> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/IMissionCategoryService.cs b/LctMonolith/Services/Contracts/IMissionCategoryService.cs index 2e751d0..f091ce2 100644 --- a/LctMonolith/Services/Contracts/IMissionCategoryService.cs +++ b/LctMonolith/Services/Contracts/IMissionCategoryService.cs @@ -1,14 +1,14 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IMissionCategoryService -{ - // CRUD should be enough - Task GetCategoryByIdAsync(Guid categoryId); - Task GetCategoryByTitleAsync(string title); - Task> GetAllCategoriesAsync(); - Task CreateCategoryAsync(MissionCategory category); - Task UpdateCategoryAsync(MissionCategory category); - Task DeleteCategoryAsync(Guid categoryId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IMissionCategoryService +{ + // CRUD should be enough + Task GetCategoryByIdAsync(Guid categoryId); + Task GetCategoryByTitleAsync(string title); + Task> GetAllCategoriesAsync(); + Task CreateCategoryAsync(MissionCategory category); + Task UpdateCategoryAsync(MissionCategory category); + Task DeleteCategoryAsync(Guid categoryId); +} diff --git a/LctMonolith/Services/Contracts/IMissionService.cs b/LctMonolith/Services/Contracts/IMissionService.cs index 0e09f45..5f15f5d 100644 --- a/LctMonolith/Services/Contracts/IMissionService.cs +++ b/LctMonolith/Services/Contracts/IMissionService.cs @@ -1,17 +1,17 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; - -namespace LctMonolith.Services.Interfaces; - -public interface IMissionService -{ - Task GetMissionByIdAsync(Guid missionId); - Task> GetMissionsByCategoryAsync(Guid categoryId); - Task> GetAvailableMissionsForPlayerAsync(Guid playerId); - Task> GetChildMissionsAsync(Guid parentMissionId); - Task CreateMissionAsync(Mission mission); - Task UpdateMissionAsync(Mission mission); - Task DeleteMissionAsync(Guid missionId); - Task IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId); - Task CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null); -} +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; + +namespace LctMonolith.Services.Interfaces; + +public interface IMissionService +{ + Task GetMissionByIdAsync(Guid missionId); + Task> GetMissionsByCategoryAsync(Guid categoryId); + Task> GetAvailableMissionsForPlayerAsync(Guid playerId); + Task> GetChildMissionsAsync(Guid parentMissionId); + Task CreateMissionAsync(Mission mission); + Task UpdateMissionAsync(Mission mission); + Task DeleteMissionAsync(Guid missionId); + Task IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId); + Task CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null); +} diff --git a/LctMonolith/Services/Contracts/INotificationService.cs b/LctMonolith/Services/Contracts/INotificationService.cs index 1da65f9..0469029 100644 --- a/LctMonolith/Services/Contracts/INotificationService.cs +++ b/LctMonolith/Services/Contracts/INotificationService.cs @@ -1,12 +1,12 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Contracts; - -public interface INotificationService -{ - Task CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default); - Task> GetUnreadAsync(Guid userId, CancellationToken ct = default); - Task> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default); - Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default); - Task MarkAllReadAsync(Guid userId, CancellationToken ct = default); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Contracts; + +public interface INotificationService +{ + Task CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default); + Task> GetUnreadAsync(Guid userId, CancellationToken ct = default); + Task> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default); + Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default); + Task MarkAllReadAsync(Guid userId, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/IPlayerService.cs b/LctMonolith/Services/Contracts/IPlayerService.cs index 708ebbe..0527d7d 100644 --- a/LctMonolith/Services/Contracts/IPlayerService.cs +++ b/LctMonolith/Services/Contracts/IPlayerService.cs @@ -1,14 +1,14 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IPlayerService -{ - Task GetPlayerByUserIdAsync(string userId); - Task CreatePlayerAsync(string userId, string username); - Task UpdatePlayerRankAsync(Guid playerId, Guid newRankId); - Task AddPlayerExperienceAsync(Guid playerId, int experience); - Task AddPlayerManaAsync(Guid playerId, int mana); - Task> GetTopPlayersAsync(int topCount, TimeSpan timeFrame); - Task GetPlayerWithProgressAsync(Guid playerId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IPlayerService +{ + Task GetPlayerByUserIdAsync(string userId); + Task CreatePlayerAsync(string userId, string username); + Task UpdatePlayerRankAsync(Guid playerId, Guid newRankId); + Task AddPlayerExperienceAsync(Guid playerId, int experience); + Task AddPlayerManaAsync(Guid playerId, int mana); + Task> GetTopPlayersAsync(int topCount, TimeSpan timeFrame); + Task GetPlayerWithProgressAsync(Guid playerId); +} diff --git a/LctMonolith/Services/Contracts/IProfileService.cs b/LctMonolith/Services/Contracts/IProfileService.cs index 11e2cde..c8c4d7a 100644 --- a/LctMonolith/Services/Contracts/IProfileService.cs +++ b/LctMonolith/Services/Contracts/IProfileService.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Services.Interfaces; - -using LctMonolith.Models.Database; - -public interface IProfileService -{ - Task GetByUserIdAsync(Guid userId, CancellationToken ct = default); - Task UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default); - Task UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default); - Task DeleteAvatarAsync(Guid userId, CancellationToken ct = default); -} +namespace LctMonolith.Services.Interfaces; + +using LctMonolith.Models.Database; + +public interface IProfileService +{ + Task GetByUserIdAsync(Guid userId, CancellationToken ct = default); + Task UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default); + Task UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default); + Task DeleteAvatarAsync(Guid userId, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/IProgressTrackingService.cs b/LctMonolith/Services/Contracts/IProgressTrackingService.cs index 5f975f1..cb1e701 100644 --- a/LctMonolith/Services/Contracts/IProgressTrackingService.cs +++ b/LctMonolith/Services/Contracts/IProgressTrackingService.cs @@ -1,14 +1,14 @@ -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; - -namespace LctMonolith.Services.Interfaces; - -public interface IProgressTrackingService -{ - Task StartMissionAsync(Guid missionId, Guid playerId); - Task UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null); - Task CompleteMissionAsync(Guid playerMissionId, object? proof = null); - Task> GetPlayerMissionsAsync(Guid playerId); - Task GetPlayerMissionAsync(Guid playerId, Guid missionId); - Task GetPlayerOverallProgressAsync(Guid playerId); -} +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; + +namespace LctMonolith.Services.Interfaces; + +public interface IProgressTrackingService +{ + Task StartMissionAsync(Guid missionId, Guid playerId); + Task UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null); + Task CompleteMissionAsync(Guid playerMissionId, object? proof = null); + Task> GetPlayerMissionsAsync(Guid playerId); + Task GetPlayerMissionAsync(Guid playerId, Guid missionId); + Task GetPlayerOverallProgressAsync(Guid playerId); +} diff --git a/LctMonolith/Services/Contracts/IRankService.cs b/LctMonolith/Services/Contracts/IRankService.cs index 91e1f9c..2abfe86 100644 --- a/LctMonolith/Services/Contracts/IRankService.cs +++ b/LctMonolith/Services/Contracts/IRankService.cs @@ -1,15 +1,15 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IRankService -{ - Task GetRankByIdAsync(Guid rankId); - Task GetRankByTitleAsync(string title); - Task> GetAllRanksAsync(); - Task CreateRankAsync(Rank rank); - Task UpdateRankAsync(Rank rank); - Task DeleteRankAsync(Guid rankId); - Task CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId); - Task GetNextRankAsync(Guid currentRankId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IRankService +{ + Task GetRankByIdAsync(Guid rankId); + Task GetRankByTitleAsync(string title); + Task> GetAllRanksAsync(); + Task CreateRankAsync(Rank rank); + Task UpdateRankAsync(Rank rank); + Task DeleteRankAsync(Guid rankId); + Task CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId); + Task GetNextRankAsync(Guid currentRankId); +} diff --git a/LctMonolith/Services/Contracts/IRewardService.cs b/LctMonolith/Services/Contracts/IRewardService.cs index 5ab5876..7480b91 100644 --- a/LctMonolith/Services/Contracts/IRewardService.cs +++ b/LctMonolith/Services/Contracts/IRewardService.cs @@ -1,11 +1,11 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IRewardService -{ - Task> GetMissionSkillRewardsAsync(Guid missionId); - Task> GetMissionItemRewardsAsync(Guid missionId); - Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId); - Task CanClaimRewardAsync(Guid rewardId, Guid playerId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IRewardService +{ + Task> GetMissionSkillRewardsAsync(Guid missionId); + Task> GetMissionItemRewardsAsync(Guid missionId); + Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId); + Task CanClaimRewardAsync(Guid rewardId, Guid playerId); +} diff --git a/LctMonolith/Services/Contracts/IRuleValidationService.cs b/LctMonolith/Services/Contracts/IRuleValidationService.cs index f83a37f..893b8b6 100644 --- a/LctMonolith/Services/Contracts/IRuleValidationService.cs +++ b/LctMonolith/Services/Contracts/IRuleValidationService.cs @@ -1,10 +1,10 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface IRuleValidationService -{ - Task ValidateMissionRankRulesAsync(Guid missionId, Guid playerId); - Task ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId); - Task> GetApplicableRankRulesAsync(Guid missionId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface IRuleValidationService +{ + Task ValidateMissionRankRulesAsync(Guid missionId, Guid playerId); + Task ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId); + Task> GetApplicableRankRulesAsync(Guid missionId); +} diff --git a/LctMonolith/Services/Contracts/ISkillService.cs b/LctMonolith/Services/Contracts/ISkillService.cs index f3495a3..5ead64d 100644 --- a/LctMonolith/Services/Contracts/ISkillService.cs +++ b/LctMonolith/Services/Contracts/ISkillService.cs @@ -1,16 +1,16 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Interfaces; - -public interface ISkillService -{ - Task GetSkillByIdAsync(Guid skillId); - Task GetSkillByTitleAsync(string title); - Task> GetAllSkillsAsync(); - Task CreateSkillAsync(Skill skill); - Task UpdateSkillAsync(Skill skill); - Task DeleteSkillAsync(Guid skillId); - Task UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level); - Task> GetPlayerSkillsAsync(Guid playerId); - Task GetPlayerSkillLevelAsync(Guid playerId, Guid skillId); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Interfaces; + +public interface ISkillService +{ + Task GetSkillByIdAsync(Guid skillId); + Task GetSkillByTitleAsync(string title); + Task> GetAllSkillsAsync(); + Task CreateSkillAsync(Skill skill); + Task UpdateSkillAsync(Skill skill); + Task DeleteSkillAsync(Guid skillId); + Task UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level); + Task> GetPlayerSkillsAsync(Guid playerId); + Task GetPlayerSkillLevelAsync(Guid playerId, Guid skillId); +} diff --git a/LctMonolith/Services/Contracts/IStoreService.cs b/LctMonolith/Services/Contracts/IStoreService.cs index a3b6034..8e291d8 100644 --- a/LctMonolith/Services/Contracts/IStoreService.cs +++ b/LctMonolith/Services/Contracts/IStoreService.cs @@ -1,9 +1,9 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Contracts; - -public interface IStoreService -{ - Task> GetActiveItemsAsync(CancellationToken ct = default); - Task PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Contracts; + +public interface IStoreService +{ + Task> GetActiveItemsAsync(CancellationToken ct = default); + Task PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/Contracts/ITokenService.cs b/LctMonolith/Services/Contracts/ITokenService.cs index 07ffa5a..ef21062 100644 --- a/LctMonolith/Services/Contracts/ITokenService.cs +++ b/LctMonolith/Services/Contracts/ITokenService.cs @@ -1,11 +1,11 @@ -using LctMonolith.Models.Database; -using LctMonolith.Services.Models; - -namespace LctMonolith.Services.Contracts; - -public interface ITokenService -{ - Task IssueAsync(AppUser user, CancellationToken ct = default); - Task RefreshAsync(string refreshToken, CancellationToken ct = default); - Task RevokeAsync(string refreshToken, CancellationToken ct = default); -} +using LctMonolith.Models.Database; +using LctMonolith.Services.Models; + +namespace LctMonolith.Services.Contracts; + +public interface ITokenService +{ + Task IssueAsync(AppUser user, CancellationToken ct = default); + Task RefreshAsync(string refreshToken, CancellationToken ct = default); + Task RevokeAsync(string refreshToken, CancellationToken ct = default); +} diff --git a/LctMonolith/Services/DialogueService.cs b/LctMonolith/Services/DialogueService.cs index 20171ab..ee7a0c5 100644 --- a/LctMonolith/Services/DialogueService.cs +++ b/LctMonolith/Services/DialogueService.cs @@ -1,55 +1,55 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class DialogueService : IDialogueService -{ - private readonly IUnitOfWork _uow; - public DialogueService(IUnitOfWork uow) => _uow = uow; - - public async Task GetDialogueByMissionIdAsync(Guid missionId) - { - try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; } - } - - public async Task CreateDialogueAsync(Dialogue dialogue) - { - try - { - dialogue.Id = Guid.NewGuid(); - await _uow.Dialogues.AddAsync(dialogue); - await _uow.SaveChangesAsync(); - return dialogue; - } - catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; } - } - - public async Task GetDialogueMessageByIdAsync(Guid messageId) - { - try { return await _uow.DialogueMessages.GetByIdAsync(messageId); } - catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; } - } - - public async Task> GetResponseOptionsAsync(Guid messageId) - { - try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; } - } - - public async Task ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId) - { - try - { - var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync(); - if (option == null) return null; - if (option.DestinationDialogueMessageId == null) return null; // end branch - return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId); - } - catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class DialogueService : IDialogueService +{ + private readonly IUnitOfWork _uow; + public DialogueService(IUnitOfWork uow) => _uow = uow; + + public async Task GetDialogueByMissionIdAsync(Guid missionId) + { + try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; } + } + + public async Task CreateDialogueAsync(Dialogue dialogue) + { + try + { + dialogue.Id = Guid.NewGuid(); + await _uow.Dialogues.AddAsync(dialogue); + await _uow.SaveChangesAsync(); + return dialogue; + } + catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; } + } + + public async Task GetDialogueMessageByIdAsync(Guid messageId) + { + try { return await _uow.DialogueMessages.GetByIdAsync(messageId); } + catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; } + } + + public async Task> GetResponseOptionsAsync(Guid messageId) + { + try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; } + } + + public async Task ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId) + { + try + { + var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync(); + if (option == null) return null; + if (option.DestinationDialogueMessageId == null) return null; // end branch + return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId); + } + catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; } + } +} diff --git a/LctMonolith/Services/InventoryService.cs b/LctMonolith/Services/InventoryService.cs index 266594e..dfe6f9f 100644 --- a/LctMonolith/Services/InventoryService.cs +++ b/LctMonolith/Services/InventoryService.cs @@ -1,26 +1,26 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Contracts; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class InventoryService : IInventoryService -{ - private readonly IUnitOfWork _uow; - public InventoryService(IUnitOfWork uow) => _uow = uow; - - public async Task> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default) - { - try - { - return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct); - } - catch (Exception ex) - { - Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId); - throw; - } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Contracts; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class InventoryService : IInventoryService +{ + private readonly IUnitOfWork _uow; + public InventoryService(IUnitOfWork uow) => _uow = uow; + + public async Task> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default) + { + try + { + return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct); + } + catch (Exception ex) + { + Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId); + throw; + } + } +} diff --git a/LctMonolith/Services/MissionCategoryService.cs b/LctMonolith/Services/MissionCategoryService.cs index f626e54..d4f73a3 100644 --- a/LctMonolith/Services/MissionCategoryService.cs +++ b/LctMonolith/Services/MissionCategoryService.cs @@ -1,49 +1,49 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class MissionCategoryService : IMissionCategoryService -{ - private readonly IUnitOfWork _uow; - public MissionCategoryService(IUnitOfWork uow) => _uow = uow; - - public async Task GetCategoryByIdAsync(Guid categoryId) - { - try { return await _uow.MissionCategories.GetByIdAsync(categoryId); } - catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; } - } - - public async Task GetCategoryByTitleAsync(string title) - { - try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; } - } - - public async Task> GetAllCategoriesAsync() - { - try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; } - } - - public async Task CreateCategoryAsync(MissionCategory category) - { - try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; } - catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; } - } - - public async Task UpdateCategoryAsync(MissionCategory category) - { - try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; } - catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; } - } - - public async Task DeleteCategoryAsync(Guid categoryId) - { - try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; } - catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class MissionCategoryService : IMissionCategoryService +{ + private readonly IUnitOfWork _uow; + public MissionCategoryService(IUnitOfWork uow) => _uow = uow; + + public async Task GetCategoryByIdAsync(Guid categoryId) + { + try { return await _uow.MissionCategories.GetByIdAsync(categoryId); } + catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; } + } + + public async Task GetCategoryByTitleAsync(string title) + { + try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; } + } + + public async Task> GetAllCategoriesAsync() + { + try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; } + } + + public async Task CreateCategoryAsync(MissionCategory category) + { + try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; } + catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; } + } + + public async Task UpdateCategoryAsync(MissionCategory category) + { + try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; } + catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; } + } + + public async Task DeleteCategoryAsync(Guid categoryId) + { + try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; } + catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; } + } +} diff --git a/LctMonolith/Services/MissionService.cs b/LctMonolith/Services/MissionService.cs index c930de1..17778db 100644 --- a/LctMonolith/Services/MissionService.cs +++ b/LctMonolith/Services/MissionService.cs @@ -1,165 +1,165 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class MissionService : IMissionService -{ - private readonly IUnitOfWork _uow; - private readonly IRewardService _rewardService; - private readonly IRuleValidationService _ruleValidationService; - - public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService) - { - _uow = uow; - _rewardService = rewardService; - _ruleValidationService = ruleValidationService; - } - - public async Task GetMissionByIdAsync(Guid missionId) - { - try { return await _uow.Missions.GetByIdAsync(missionId); } - catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; } - } - - public async Task> GetMissionsByCategoryAsync(Guid categoryId) - { - try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; } - } - - public async Task> GetAvailableMissionsForPlayerAsync(Guid playerId) - { - try - { - var missions = await _uow.Missions.Query().ToListAsync(); - var result = new List(); - foreach (var m in missions) - { - if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m); - } - return result; - } - catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; } - } - - public async Task> GetChildMissionsAsync(Guid parentMissionId) - { - try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; } - } - - public async Task CreateMissionAsync(Mission mission) - { - try - { - mission.Id = Guid.NewGuid(); - await _uow.Missions.AddAsync(mission); - await _uow.SaveChangesAsync(); - return mission; - } - catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; } - } - - public async Task UpdateMissionAsync(Mission mission) - { - try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; } - catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; } - } - - public async Task DeleteMissionAsync(Guid missionId) - { - try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; } - catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; } - } - - public async Task IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId) - { - try - { - var mission = await _uow.Missions.GetByIdAsync(missionId); - if (mission == null) return false; - // rule validation - if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false; - // already completed? then not available - var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync(); - if (completed) return false; - // if parent mission required ensure parent completed - if (mission.ParentMissionId.HasValue) - { - var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync(); - if (!parentDone) return false; - } - return true; - } - catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } - } - - public async Task CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null) - { - try - { - if (!await IsMissionAvailableForPlayerAsync(missionId, playerId)) - { - return new MissionCompletionResult { Success = false, Message = "Mission not available" }; - } - var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found"); - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - - // snapshot skill levels before - var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync(); - var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); - - // mark PlayerMission - var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync(); - if (pm == null) - { - pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow }; - await _uow.PlayerMissions.AddAsync(pm); - } - pm.Completed = DateTime.UtcNow; - pm.ProgressPercent = 100; - await _uow.SaveChangesAsync(); - - var prevExp = player.Experience; - var prevMana = player.Mana; - - // distribute rewards (XP/Mana/Skills/Items) - await _rewardService.DistributeMissionRewardsAsync(missionId, playerId); - await _uow.SaveChangesAsync(); - - // build skill progress - var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); - var skillProgress = new List(); - foreach (var r in skillRewards) - { - var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0; - var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before; - if (after != before) - { - var skill = await _uow.Skills.GetByIdAsync(r.SkillId); - skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after }); - } - } - - return new MissionCompletionResult - { - Success = true, - Message = "Mission completed", - ExperienceGained = player.Experience - prevExp, - ManaGained = player.Mana - prevMana, - SkillsProgress = skillProgress, - UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync()) - }; - } - catch (Exception ex) - { - Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); - throw; - } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class MissionService : IMissionService +{ + private readonly IUnitOfWork _uow; + private readonly IRewardService _rewardService; + private readonly IRuleValidationService _ruleValidationService; + + public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService) + { + _uow = uow; + _rewardService = rewardService; + _ruleValidationService = ruleValidationService; + } + + public async Task GetMissionByIdAsync(Guid missionId) + { + try { return await _uow.Missions.GetByIdAsync(missionId); } + catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; } + } + + public async Task> GetMissionsByCategoryAsync(Guid categoryId) + { + try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; } + } + + public async Task> GetAvailableMissionsForPlayerAsync(Guid playerId) + { + try + { + var missions = await _uow.Missions.Query().ToListAsync(); + var result = new List(); + foreach (var m in missions) + { + if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m); + } + return result; + } + catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; } + } + + public async Task> GetChildMissionsAsync(Guid parentMissionId) + { + try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; } + } + + public async Task CreateMissionAsync(Mission mission) + { + try + { + mission.Id = Guid.NewGuid(); + await _uow.Missions.AddAsync(mission); + await _uow.SaveChangesAsync(); + return mission; + } + catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; } + } + + public async Task UpdateMissionAsync(Mission mission) + { + try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; } + catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; } + } + + public async Task DeleteMissionAsync(Guid missionId) + { + try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; } + catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; } + } + + public async Task IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId) + { + try + { + var mission = await _uow.Missions.GetByIdAsync(missionId); + if (mission == null) return false; + // rule validation + if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false; + // already completed? then not available + var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync(); + if (completed) return false; + // if parent mission required ensure parent completed + if (mission.ParentMissionId.HasValue) + { + var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync(); + if (!parentDone) return false; + } + return true; + } + catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } + } + + public async Task CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null) + { + try + { + if (!await IsMissionAvailableForPlayerAsync(missionId, playerId)) + { + return new MissionCompletionResult { Success = false, Message = "Mission not available" }; + } + var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found"); + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + + // snapshot skill levels before + var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync(); + var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); + + // mark PlayerMission + var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync(); + if (pm == null) + { + pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow }; + await _uow.PlayerMissions.AddAsync(pm); + } + pm.Completed = DateTime.UtcNow; + pm.ProgressPercent = 100; + await _uow.SaveChangesAsync(); + + var prevExp = player.Experience; + var prevMana = player.Mana; + + // distribute rewards (XP/Mana/Skills/Items) + await _rewardService.DistributeMissionRewardsAsync(missionId, playerId); + await _uow.SaveChangesAsync(); + + // build skill progress + var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); + var skillProgress = new List(); + foreach (var r in skillRewards) + { + var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0; + var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before; + if (after != before) + { + var skill = await _uow.Skills.GetByIdAsync(r.SkillId); + skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after }); + } + } + + return new MissionCompletionResult + { + Success = true, + Message = "Mission completed", + ExperienceGained = player.Experience - prevExp, + ManaGained = player.Mana - prevMana, + SkillsProgress = skillProgress, + UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync()) + }; + } + catch (Exception ex) + { + Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); + throw; + } + } +} diff --git a/LctMonolith/Services/Models/AnalyticsSummary.cs b/LctMonolith/Services/Models/AnalyticsSummary.cs index 0478a52..8d409b3 100644 --- a/LctMonolith/Services/Models/AnalyticsSummary.cs +++ b/LctMonolith/Services/Models/AnalyticsSummary.cs @@ -1,11 +1,11 @@ -namespace LctMonolith.Services.Models; - -public class AnalyticsSummary -{ - public int TotalUsers { get; set; } - public int TotalMissions { get; set; } - public int CompletedMissions { get; set; } - public int TotalStoreItems { get; set; } - public long TotalExperience { get; set; } - public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow; -} +namespace LctMonolith.Services.Models; + +public class AnalyticsSummary +{ + public int TotalUsers { get; set; } + public int TotalMissions { get; set; } + public int CompletedMissions { get; set; } + public int TotalStoreItems { get; set; } + public long TotalExperience { get; set; } + public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow; +} diff --git a/LctMonolith/Services/Models/AuthRequest.cs b/LctMonolith/Services/Models/AuthRequest.cs index cab5737..8085607 100644 --- a/LctMonolith/Services/Models/AuthRequest.cs +++ b/LctMonolith/Services/Models/AuthRequest.cs @@ -1,9 +1,9 @@ -namespace LctMonolith.Services.Models; - -public class AuthRequest -{ - public string Email { get; set; } = null!; - public string Password { get; set; } = null!; - public string? FirstName { get; set; } - public string? LastName { get; set; } -} +namespace LctMonolith.Services.Models; + +public class AuthRequest +{ + public string Email { get; set; } = null!; + public string Password { get; set; } = null!; + public string? FirstName { get; set; } + public string? LastName { get; set; } +} diff --git a/LctMonolith/Services/Models/CompetencyRewardModel.cs b/LctMonolith/Services/Models/CompetencyRewardModel.cs index ce9cd05..9816da3 100644 --- a/LctMonolith/Services/Models/CompetencyRewardModel.cs +++ b/LctMonolith/Services/Models/CompetencyRewardModel.cs @@ -1,8 +1,8 @@ -namespace LctMonolith.Services.Models; - -public class CompetencyRewardModel -{ - public Guid CompetencyId { get; set; } - public int LevelDelta { get; set; } - public int ProgressPointsDelta { get; set; } -} +namespace LctMonolith.Services.Models; + +public class CompetencyRewardModel +{ + public Guid CompetencyId { get; set; } + public int LevelDelta { get; set; } + public int ProgressPointsDelta { get; set; } +} diff --git a/LctMonolith/Services/Models/CreateMissionModel.cs b/LctMonolith/Services/Models/CreateMissionModel.cs index 52b2fc7..86c0b94 100644 --- a/LctMonolith/Services/Models/CreateMissionModel.cs +++ b/LctMonolith/Services/Models/CreateMissionModel.cs @@ -1,15 +1,15 @@ -using LctMonolith.Models.Database; - -namespace LctMonolith.Services.Models; - -public class CreateMissionModel -{ - public string Title { get; set; } = null!; - public string? Description { get; set; } - public string? Branch { get; set; } - public MissionCategory Category { get; set; } - public Guid? MinRankId { get; set; } - public int ExperienceReward { get; set; } - public int ManaReward { get; set; } - public List CompetencyRewards { get; set; } = new(); -} +using LctMonolith.Models.Database; + +namespace LctMonolith.Services.Models; + +public class CreateMissionModel +{ + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string? Branch { get; set; } + public MissionCategory Category { get; set; } + public Guid? MinRankId { get; set; } + public int ExperienceReward { get; set; } + public int ManaReward { get; set; } + public List CompetencyRewards { get; set; } = new(); +} diff --git a/LctMonolith/Services/Models/ProgressSnapshot.cs b/LctMonolith/Services/Models/ProgressSnapshot.cs index 7fe960e..0955c94 100644 --- a/LctMonolith/Services/Models/ProgressSnapshot.cs +++ b/LctMonolith/Services/Models/ProgressSnapshot.cs @@ -1,23 +1,23 @@ -namespace LctMonolith.Services.Models; - -public class ProgressSnapshot -{ - public int Experience { get; set; } - public int Mana { get; set; } - public Guid? CurrentRankId { get; set; } - public string? CurrentRankName { get; set; } - public Guid? NextRankId { get; set; } - public string? NextRankName { get; set; } - public int? RequiredExperienceForNextRank { get; set; } - public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null; - public List OutstandingMissionIds { get; set; } = new(); - public List OutstandingCompetencies { get; set; } = new(); -} - -public class OutstandingCompetency -{ - public Guid CompetencyId { get; set; } - public string? CompetencyName { get; set; } - public int RequiredLevel { get; set; } - public int CurrentLevel { get; set; } -} +namespace LctMonolith.Services.Models; + +public class ProgressSnapshot +{ + public int Experience { get; set; } + public int Mana { get; set; } + public Guid? CurrentRankId { get; set; } + public string? CurrentRankName { get; set; } + public Guid? NextRankId { get; set; } + public string? NextRankName { get; set; } + public int? RequiredExperienceForNextRank { get; set; } + public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null; + public List OutstandingMissionIds { get; set; } = new(); + public List OutstandingCompetencies { get; set; } = new(); +} + +public class OutstandingCompetency +{ + public Guid CompetencyId { get; set; } + public string? CompetencyName { get; set; } + public int RequiredLevel { get; set; } + public int CurrentLevel { get; set; } +} diff --git a/LctMonolith/Services/Models/PurchaseRequest.cs b/LctMonolith/Services/Models/PurchaseRequest.cs index 61ceb7e..c2bf553 100644 --- a/LctMonolith/Services/Models/PurchaseRequest.cs +++ b/LctMonolith/Services/Models/PurchaseRequest.cs @@ -1,7 +1,7 @@ -namespace LctMonolith.Services.Models; - -public class PurchaseRequest -{ - public Guid ItemId { get; set; } - public int Quantity { get; set; } = 1; -} +namespace LctMonolith.Services.Models; + +public class PurchaseRequest +{ + public Guid ItemId { get; set; } + public int Quantity { get; set; } = 1; +} diff --git a/LctMonolith/Services/Models/RefreshRequest.cs b/LctMonolith/Services/Models/RefreshRequest.cs index dd1fb95..87f51b5 100644 --- a/LctMonolith/Services/Models/RefreshRequest.cs +++ b/LctMonolith/Services/Models/RefreshRequest.cs @@ -1,6 +1,6 @@ -namespace LctMonolith.Services.Models; - -public class RefreshRequest -{ - public string RefreshToken { get; set; } = null!; -} +namespace LctMonolith.Services.Models; + +public class RefreshRequest +{ + public string RefreshToken { get; set; } = null!; +} diff --git a/LctMonolith/Services/Models/RevokeRequest.cs b/LctMonolith/Services/Models/RevokeRequest.cs index 0b0aa49..a6c1ab8 100644 --- a/LctMonolith/Services/Models/RevokeRequest.cs +++ b/LctMonolith/Services/Models/RevokeRequest.cs @@ -1,6 +1,6 @@ -namespace LctMonolith.Services.Models; - -public class RevokeRequest -{ - public string RefreshToken { get; set; } = null!; -} +namespace LctMonolith.Services.Models; + +public class RevokeRequest +{ + public string RefreshToken { get; set; } = null!; +} diff --git a/LctMonolith/Services/Models/TokenPair.cs b/LctMonolith/Services/Models/TokenPair.cs index 5b00a6f..5d5d064 100644 --- a/LctMonolith/Services/Models/TokenPair.cs +++ b/LctMonolith/Services/Models/TokenPair.cs @@ -1,3 +1,3 @@ -namespace LctMonolith.Services.Models; - -public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt); +namespace LctMonolith.Services.Models; + +public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt); diff --git a/LctMonolith/Services/NotificationService.cs b/LctMonolith/Services/NotificationService.cs index 80d458c..61f13cf 100644 --- a/LctMonolith/Services/NotificationService.cs +++ b/LctMonolith/Services/NotificationService.cs @@ -1,68 +1,68 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Contracts; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -/// -/// In-app user notifications CRUD / read-state operations. -/// -public class NotificationService : INotificationService -{ - private readonly IUnitOfWork _uow; - - public NotificationService(IUnitOfWork uow) - { - _uow = uow; - } - - public async Task CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default) - { - try - { - if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type)); - if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title)); - if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message)); - - var n = new Notification - { - UserId = userId, - Type = type.Trim(), - Title = title.Trim(), - Message = message.Trim(), - CreatedAt = DateTime.UtcNow, - IsRead = false - }; - await _uow.Notifications.AddAsync(n, ct); - await _uow.SaveChangesAsync(ct); - return n; - } - catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; } - } - - public async Task> GetUnreadAsync(Guid userId, CancellationToken ct = default) - { - try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); } - catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; } - } - - public async Task> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default) - { - try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); } - catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; } - } - - public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default) - { - try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } } - catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; } - } - - public async Task MarkAllReadAsync(Guid userId, CancellationToken ct = default) - { - try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; } - catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Contracts; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +/// +/// In-app user notifications CRUD / read-state operations. +/// +public class NotificationService : INotificationService +{ + private readonly IUnitOfWork _uow; + + public NotificationService(IUnitOfWork uow) + { + _uow = uow; + } + + public async Task CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default) + { + try + { + if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type)); + if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title)); + if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message)); + + var n = new Notification + { + UserId = userId, + Type = type.Trim(), + Title = title.Trim(), + Message = message.Trim(), + CreatedAt = DateTime.UtcNow, + IsRead = false + }; + await _uow.Notifications.AddAsync(n, ct); + await _uow.SaveChangesAsync(ct); + return n; + } + catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; } + } + + public async Task> GetUnreadAsync(Guid userId, CancellationToken ct = default) + { + try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); } + catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; } + } + + public async Task> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default) + { + try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); } + catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; } + } + + public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default) + { + try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } } + catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; } + } + + public async Task MarkAllReadAsync(Guid userId, CancellationToken ct = default) + { + try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; } + catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; } + } +} diff --git a/LctMonolith/Services/PlayerService.cs b/LctMonolith/Services/PlayerService.cs index 5780d9d..181b46b 100644 --- a/LctMonolith/Services/PlayerService.cs +++ b/LctMonolith/Services/PlayerService.cs @@ -1,153 +1,153 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class PlayerService : IPlayerService -{ - private readonly IUnitOfWork _uow; - public PlayerService(IUnitOfWork uow) => _uow = uow; - - public async Task GetPlayerByUserIdAsync(string userId) - { - try - { - if (!Guid.TryParse(userId, out var uid)) return null; - return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId); - throw; - } - } - - public async Task CreatePlayerAsync(string userId, string username) - { - try - { - if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId)); - var existing = await GetPlayerByUserIdAsync(userId); - if (existing != null) return existing; - // pick lowest exp rank - var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync(); - var player = new Player - { - Id = Guid.NewGuid(), - UserId = uid, - RankId = baseRank.Id, - Experience = 0, - Mana = 0 - }; - await _uow.Players.AddAsync(player); - await _uow.SaveChangesAsync(); - Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId); - return player; - } - catch (Exception ex) - { - Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId); - throw; - } - } - - public async Task UpdatePlayerRankAsync(Guid playerId, Guid newRankId) - { - try - { - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found"); - player.RankId = rank.Id; - await _uow.SaveChangesAsync(); - return player; - } - catch (Exception ex) - { - Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId); - throw; - } - } - - public async Task AddPlayerExperienceAsync(Guid playerId, int experience) - { - try - { - if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException(); - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - player.Experience += experience; - await _uow.SaveChangesAsync(); - await AutoRankUpAsync(player); - return player; - } - catch (Exception ex) - { - Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId); - throw; - } - } - - private async Task AutoRankUpAsync(Player player) - { - // find highest rank whose ExpNeeded <= player's experience - var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience) - .OrderByDescending(r => r.ExpNeeded) - .FirstOrDefaultAsync(); - if (target != null && target.Id != player.RankId) - { - player.RankId = target.Id; - await _uow.SaveChangesAsync(); - Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title); - } - } - - public async Task AddPlayerManaAsync(Guid playerId, int mana) - { - try - { - if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException(); - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - player.Mana += mana; - await _uow.SaveChangesAsync(); - return player; - } - catch (Exception ex) - { - Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId); - throw; - } - } - - public async Task> GetTopPlayersAsync(int topCount, TimeSpan timeFrame) - { - try - { - // Simple ordering by experience (timeFrame ignored due to no timestamp on Player) - return await _uow.Players.Query() - .OrderByDescending(p => p.Experience) - .Take(topCount) - .ToListAsync(); - } - catch (Exception ex) - { - Log.Error(ex, "GetTopPlayersAsync failed"); - throw; - } - } - - public async Task GetPlayerWithProgressAsync(Guid playerId) - { - try - { - return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank) - .FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found"); - } - catch (Exception ex) - { - Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId); - throw; - } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class PlayerService : IPlayerService +{ + private readonly IUnitOfWork _uow; + public PlayerService(IUnitOfWork uow) => _uow = uow; + + public async Task GetPlayerByUserIdAsync(string userId) + { + try + { + if (!Guid.TryParse(userId, out var uid)) return null; + return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId); + throw; + } + } + + public async Task CreatePlayerAsync(string userId, string username) + { + try + { + if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId)); + var existing = await GetPlayerByUserIdAsync(userId); + if (existing != null) return existing; + // pick lowest exp rank + var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync(); + var player = new Player + { + Id = Guid.NewGuid(), + UserId = uid, + RankId = baseRank.Id, + Experience = 0, + Mana = 0 + }; + await _uow.Players.AddAsync(player); + await _uow.SaveChangesAsync(); + Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId); + return player; + } + catch (Exception ex) + { + Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId); + throw; + } + } + + public async Task UpdatePlayerRankAsync(Guid playerId, Guid newRankId) + { + try + { + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found"); + player.RankId = rank.Id; + await _uow.SaveChangesAsync(); + return player; + } + catch (Exception ex) + { + Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId); + throw; + } + } + + public async Task AddPlayerExperienceAsync(Guid playerId, int experience) + { + try + { + if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException(); + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + player.Experience += experience; + await _uow.SaveChangesAsync(); + await AutoRankUpAsync(player); + return player; + } + catch (Exception ex) + { + Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId); + throw; + } + } + + private async Task AutoRankUpAsync(Player player) + { + // find highest rank whose ExpNeeded <= player's experience + var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience) + .OrderByDescending(r => r.ExpNeeded) + .FirstOrDefaultAsync(); + if (target != null && target.Id != player.RankId) + { + player.RankId = target.Id; + await _uow.SaveChangesAsync(); + Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title); + } + } + + public async Task AddPlayerManaAsync(Guid playerId, int mana) + { + try + { + if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException(); + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + player.Mana += mana; + await _uow.SaveChangesAsync(); + return player; + } + catch (Exception ex) + { + Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId); + throw; + } + } + + public async Task> GetTopPlayersAsync(int topCount, TimeSpan timeFrame) + { + try + { + // Simple ordering by experience (timeFrame ignored due to no timestamp on Player) + return await _uow.Players.Query() + .OrderByDescending(p => p.Experience) + .Take(topCount) + .ToListAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "GetTopPlayersAsync failed"); + throw; + } + } + + public async Task GetPlayerWithProgressAsync(Guid playerId) + { + try + { + return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank) + .FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found"); + } + catch (Exception ex) + { + Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId); + throw; + } + } +} diff --git a/LctMonolith/Services/ProfileService.cs b/LctMonolith/Services/ProfileService.cs index 430fcf1..7168d0e 100644 --- a/LctMonolith/Services/ProfileService.cs +++ b/LctMonolith/Services/ProfileService.cs @@ -1,121 +1,121 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Serilog; -using Microsoft.EntityFrameworkCore; -using System.Linq; - -namespace LctMonolith.Services; - -public class ProfileService : IProfileService -{ - private readonly IUnitOfWork _uow; - private readonly IFileStorageService _storage; - - public ProfileService(IUnitOfWork uow, IFileStorageService storage) - { - _uow = uow; - _storage = storage; - } - - public async Task GetByUserIdAsync(Guid userId, CancellationToken ct = default) - { - try - { - return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); - } - catch (Exception ex) - { - Log.Error(ex, "Profile get failed {UserId}", userId); - throw; - } - } - - public async Task UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default) - { - try - { - var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); - if (profile == null) - { - profile = new Profile - { - Id = Guid.NewGuid(), - UserId = userId, - FirstName = firstName, - LastName = lastName, - BirthDate = birthDate, - About = about, - Location = location - }; - await _uow.Profiles.AddAsync(profile, ct); - } - else - { - profile.FirstName = firstName; - profile.LastName = lastName; - profile.BirthDate = birthDate; - profile.About = about; - profile.Location = location; - profile.UpdatedAt = DateTime.UtcNow; - _uow.Profiles.Update(profile); - } - await _uow.SaveChangesAsync(ct); - return profile; - } - catch (Exception ex) - { - Log.Error(ex, "Profile upsert failed {UserId}", userId); - throw; - } - } - - public async Task UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default) - { - try - { - var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? - await UpsertAsync(userId, null, null, null, null, null, ct); - - // Delete old if exists - if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key)) - { - await _storage.DeleteAsync(profile.AvatarS3Key!, ct); - } - var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct); - var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct); - profile.AvatarS3Key = key; - profile.AvatarUrl = url; - profile.UpdatedAt = DateTime.UtcNow; - _uow.Profiles.Update(profile); - await _uow.SaveChangesAsync(ct); - return profile; - } - catch (Exception ex) - { - Log.Error(ex, "Avatar update failed {UserId}", userId); - throw; - } - } - - public async Task DeleteAvatarAsync(Guid userId, CancellationToken ct = default) - { - try - { - var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); - if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false; - await _storage.DeleteAsync(profile.AvatarS3Key!, ct); - profile.AvatarS3Key = null; - profile.AvatarUrl = null; - profile.UpdatedAt = DateTime.UtcNow; - _uow.Profiles.Update(profile); - await _uow.SaveChangesAsync(ct); - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Delete avatar failed {UserId}", userId); - throw; - } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Serilog; +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace LctMonolith.Services; + +public class ProfileService : IProfileService +{ + private readonly IUnitOfWork _uow; + private readonly IFileStorageService _storage; + + public ProfileService(IUnitOfWork uow, IFileStorageService storage) + { + _uow = uow; + _storage = storage; + } + + public async Task GetByUserIdAsync(Guid userId, CancellationToken ct = default) + { + try + { + return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); + } + catch (Exception ex) + { + Log.Error(ex, "Profile get failed {UserId}", userId); + throw; + } + } + + public async Task UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default) + { + try + { + var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); + if (profile == null) + { + profile = new Profile + { + Id = Guid.NewGuid(), + UserId = userId, + FirstName = firstName, + LastName = lastName, + BirthDate = birthDate, + About = about, + Location = location + }; + await _uow.Profiles.AddAsync(profile, ct); + } + else + { + profile.FirstName = firstName; + profile.LastName = lastName; + profile.BirthDate = birthDate; + profile.About = about; + profile.Location = location; + profile.UpdatedAt = DateTime.UtcNow; + _uow.Profiles.Update(profile); + } + await _uow.SaveChangesAsync(ct); + return profile; + } + catch (Exception ex) + { + Log.Error(ex, "Profile upsert failed {UserId}", userId); + throw; + } + } + + public async Task UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default) + { + try + { + var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? + await UpsertAsync(userId, null, null, null, null, null, ct); + + // Delete old if exists + if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key)) + { + await _storage.DeleteAsync(profile.AvatarS3Key!, ct); + } + var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct); + var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct); + profile.AvatarS3Key = key; + profile.AvatarUrl = url; + profile.UpdatedAt = DateTime.UtcNow; + _uow.Profiles.Update(profile); + await _uow.SaveChangesAsync(ct); + return profile; + } + catch (Exception ex) + { + Log.Error(ex, "Avatar update failed {UserId}", userId); + throw; + } + } + + public async Task DeleteAvatarAsync(Guid userId, CancellationToken ct = default) + { + try + { + var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct); + if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false; + await _storage.DeleteAsync(profile.AvatarS3Key!, ct); + profile.AvatarS3Key = null; + profile.AvatarUrl = null; + profile.UpdatedAt = DateTime.UtcNow; + _uow.Profiles.Update(profile); + await _uow.SaveChangesAsync(ct); + return true; + } + catch (Exception ex) + { + Log.Error(ex, "Delete avatar failed {UserId}", userId); + throw; + } + } +} diff --git a/LctMonolith/Services/ProgressTrackingService.cs b/LctMonolith/Services/ProgressTrackingService.cs index 1a8ae20..1fb37b8 100644 --- a/LctMonolith/Services/ProgressTrackingService.cs +++ b/LctMonolith/Services/ProgressTrackingService.cs @@ -1,103 +1,103 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Models.DTO; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class ProgressTrackingService : IProgressTrackingService -{ - private readonly IUnitOfWork _uow; - private readonly IMissionService _missionService; - - public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService) - { - _uow = uow; - _missionService = missionService; - } - - public async Task StartMissionAsync(Guid missionId, Guid playerId) - { - try - { - var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); - if (existing != null) return existing; - var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 }; - await _uow.PlayerMissions.AddAsync(pm); - await _uow.SaveChangesAsync(); - return pm; - } - catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } - } - - public async Task UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null) - { - try - { - if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage)); - var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found"); - if (pm.Completed != null) return pm; - pm.ProgressPercent = progressPercentage; - if (progressPercentage == 100 && pm.Completed == null) - { - // Complete mission through mission service to allocate rewards, etc. - await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId); - } - await _uow.SaveChangesAsync(); - return pm; - } - catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; } - } - - public async Task CompleteMissionAsync(Guid playerMissionId, object? proof = null) - { - try - { - var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found"); - if (pm.Completed != null) return pm; - await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof); - await _uow.SaveChangesAsync(); - return pm; - } - catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; } - } - - public async Task> GetPlayerMissionsAsync(Guid playerId) - { - try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; } - } - - public async Task GetPlayerMissionAsync(Guid playerId, Guid missionId) - { - try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } - } - - public async Task GetPlayerOverallProgressAsync(Guid playerId) - { - try - { - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); - var completed = missions.Count(m => m.Completed != null); - var totalMissions = await _uow.Missions.Query().CountAsync(); - var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill) - .ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score); - return new PlayerProgress - { - PlayerId = playerId, - PlayerName = playerId.ToString(), - CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId), - TotalExperience = player.Experience, - TotalMana = player.Mana, - CompletedMissions = completed, - TotalAvailableMissions = totalMissions, - SkillLevels = skillLevels - }; - } - catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Models.DTO; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class ProgressTrackingService : IProgressTrackingService +{ + private readonly IUnitOfWork _uow; + private readonly IMissionService _missionService; + + public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService) + { + _uow = uow; + _missionService = missionService; + } + + public async Task StartMissionAsync(Guid missionId, Guid playerId) + { + try + { + var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); + if (existing != null) return existing; + var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 }; + await _uow.PlayerMissions.AddAsync(pm); + await _uow.SaveChangesAsync(); + return pm; + } + catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } + } + + public async Task UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null) + { + try + { + if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage)); + var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found"); + if (pm.Completed != null) return pm; + pm.ProgressPercent = progressPercentage; + if (progressPercentage == 100 && pm.Completed == null) + { + // Complete mission through mission service to allocate rewards, etc. + await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId); + } + await _uow.SaveChangesAsync(); + return pm; + } + catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; } + } + + public async Task CompleteMissionAsync(Guid playerMissionId, object? proof = null) + { + try + { + var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found"); + if (pm.Completed != null) return pm; + await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof); + await _uow.SaveChangesAsync(); + return pm; + } + catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; } + } + + public async Task> GetPlayerMissionsAsync(Guid playerId) + { + try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; } + } + + public async Task GetPlayerMissionAsync(Guid playerId, Guid missionId) + { + try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } + } + + public async Task GetPlayerOverallProgressAsync(Guid playerId) + { + try + { + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); + var completed = missions.Count(m => m.Completed != null); + var totalMissions = await _uow.Missions.Query().CountAsync(); + var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill) + .ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score); + return new PlayerProgress + { + PlayerId = playerId, + PlayerName = playerId.ToString(), + CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId), + TotalExperience = player.Experience, + TotalMana = player.Mana, + CompletedMissions = completed, + TotalAvailableMissions = totalMissions, + SkillLevels = skillLevels + }; + } + catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; } + } +} diff --git a/LctMonolith/Services/RankService.cs b/LctMonolith/Services/RankService.cs index 55b5c98..c026c26 100644 --- a/LctMonolith/Services/RankService.cs +++ b/LctMonolith/Services/RankService.cs @@ -1,75 +1,75 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class RankService : IRankService -{ - private readonly IUnitOfWork _uow; - public RankService(IUnitOfWork uow) => _uow = uow; - - public async Task GetRankByIdAsync(Guid rankId) - { - try { return await _uow.Ranks.GetByIdAsync(rankId); } - catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; } - } - public async Task GetRankByTitleAsync(string title) - { - try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; } - } - public async Task> GetAllRanksAsync() - { - try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; } - } - public async Task CreateRankAsync(Rank rank) - { - try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; } - catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; } - } - public async Task UpdateRankAsync(Rank rank) - { - try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; } - catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; } - } - public async Task DeleteRankAsync(Guid rankId) - { - try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; } - catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; } - } - public async Task CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId) - { - try { - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found"); - if (player.Experience < rank.ExpNeeded) return false; - var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync(); - if (missionReqs.Count > 0) - { - var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync(); - if (missionReqs.Except(completed).Any()) return false; - } - var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync(); - if (skillReqs.Count > 0) - { - var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); - foreach (var req in skillReqs) - { - var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId); - if (ps == null || ps.Score < req.Min) return false; - } - } - return true; } - catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; } - } - public async Task GetNextRankAsync(Guid currentRankId) - { - try { - var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class RankService : IRankService +{ + private readonly IUnitOfWork _uow; + public RankService(IUnitOfWork uow) => _uow = uow; + + public async Task GetRankByIdAsync(Guid rankId) + { + try { return await _uow.Ranks.GetByIdAsync(rankId); } + catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; } + } + public async Task GetRankByTitleAsync(string title) + { + try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; } + } + public async Task> GetAllRanksAsync() + { + try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; } + } + public async Task CreateRankAsync(Rank rank) + { + try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; } + catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; } + } + public async Task UpdateRankAsync(Rank rank) + { + try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; } + catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; } + } + public async Task DeleteRankAsync(Guid rankId) + { + try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; } + catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; } + } + public async Task CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId) + { + try { + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found"); + if (player.Experience < rank.ExpNeeded) return false; + var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync(); + if (missionReqs.Count > 0) + { + var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync(); + if (missionReqs.Except(completed).Any()) return false; + } + var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync(); + if (skillReqs.Count > 0) + { + var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); + foreach (var req in skillReqs) + { + var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId); + if (ps == null || ps.Score < req.Min) return false; + } + } + return true; } + catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; } + } + public async Task GetNextRankAsync(Guid currentRankId) + { + try { + var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; } + } +} diff --git a/LctMonolith/Services/RewardService.cs b/LctMonolith/Services/RewardService.cs index 49d9bbc..12bc576 100644 --- a/LctMonolith/Services/RewardService.cs +++ b/LctMonolith/Services/RewardService.cs @@ -1,92 +1,92 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class RewardService : IRewardService -{ - private readonly IUnitOfWork _uow; - public RewardService(IUnitOfWork uow) => _uow = uow; - - public async Task> GetMissionSkillRewardsAsync(Guid missionId) - { - try { return await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId, null, r => r.Skill).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetMissionSkillRewardsAsync failed {MissionId}", missionId); throw; } - } - - public async Task> GetMissionItemRewardsAsync(Guid missionId) - { - try { return await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetMissionItemRewardsAsync failed {MissionId}", missionId); throw; } - } - - public async Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId) - { - try - { - var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found"); - var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); - - player.Experience += mission.ExpReward; - player.Mana += mission.ManaReward; - - // Skill rewards - var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync(); - foreach (var sr in skillRewards) - { - var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == sr.SkillId).FirstOrDefaultAsync(); - if (ps == null) - { - ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = sr.SkillId, Score = sr.Value }; - await _uow.PlayerSkills.AddAsync(ps); - } - else - { - ps.Score += sr.Value; - _uow.PlayerSkills.Update(ps); - } - } - - // Item rewards (store items) one each - var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); - foreach (var ir in itemRewards) - { - var inv = await _uow.UserInventoryItems.FindAsync(player.UserId, ir.ItemId); - if (inv == null) - { - inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow }; - await _uow.UserInventoryItems.AddAsync(inv); - } - else inv.Quantity += 1; - } - - // Mark redeemed - var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); - if (pm != null && pm.RewardsRedeemed == null) - { - pm.RewardsRedeemed = DateTime.UtcNow; - _uow.PlayerMissions.Update(pm); - } - } - catch (Exception ex) - { - Log.Error(ex, "DistributeMissionRewardsAsync failed {MissionId} {PlayerId}", missionId, playerId); - throw; - } - } - - public async Task CanClaimRewardAsync(Guid rewardId, Guid playerId) - { - try - { - // Interpret rewardId as missionId; claim if mission completed and rewards not yet redeemed - var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == rewardId).FirstOrDefaultAsync(); - if (pm == null || pm.Completed == null) return false; - return pm.RewardsRedeemed == null; - } - catch (Exception ex) { Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class RewardService : IRewardService +{ + private readonly IUnitOfWork _uow; + public RewardService(IUnitOfWork uow) => _uow = uow; + + public async Task> GetMissionSkillRewardsAsync(Guid missionId) + { + try { return await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId, null, r => r.Skill).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetMissionSkillRewardsAsync failed {MissionId}", missionId); throw; } + } + + public async Task> GetMissionItemRewardsAsync(Guid missionId) + { + try { return await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetMissionItemRewardsAsync failed {MissionId}", missionId); throw; } + } + + public async Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId) + { + try + { + var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found"); + var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found"); + + player.Experience += mission.ExpReward; + player.Mana += mission.ManaReward; + + // Skill rewards + var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync(); + foreach (var sr in skillRewards) + { + var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == sr.SkillId).FirstOrDefaultAsync(); + if (ps == null) + { + ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = sr.SkillId, Score = sr.Value }; + await _uow.PlayerSkills.AddAsync(ps); + } + else + { + ps.Score += sr.Value; + _uow.PlayerSkills.Update(ps); + } + } + + // Item rewards (store items) one each + var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); + foreach (var ir in itemRewards) + { + var inv = await _uow.UserInventoryItems.FindAsync(player.UserId, ir.ItemId); + if (inv == null) + { + inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow }; + await _uow.UserInventoryItems.AddAsync(inv); + } + else inv.Quantity += 1; + } + + // Mark redeemed + var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); + if (pm != null && pm.RewardsRedeemed == null) + { + pm.RewardsRedeemed = DateTime.UtcNow; + _uow.PlayerMissions.Update(pm); + } + } + catch (Exception ex) + { + Log.Error(ex, "DistributeMissionRewardsAsync failed {MissionId} {PlayerId}", missionId, playerId); + throw; + } + } + + public async Task CanClaimRewardAsync(Guid rewardId, Guid playerId) + { + try + { + // Interpret rewardId as missionId; claim if mission completed and rewards not yet redeemed + var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == rewardId).FirstOrDefaultAsync(); + if (pm == null || pm.Completed == null) return false; + return pm.RewardsRedeemed == null; + } + catch (Exception ex) { Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId); throw; } + } +} diff --git a/LctMonolith/Services/RuleValidationService.cs b/LctMonolith/Services/RuleValidationService.cs index 741d71b..e580ac1 100644 --- a/LctMonolith/Services/RuleValidationService.cs +++ b/LctMonolith/Services/RuleValidationService.cs @@ -1,64 +1,64 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class RuleValidationService : IRuleValidationService -{ - private readonly IUnitOfWork _uow; - public RuleValidationService(IUnitOfWork uow) => _uow = uow; - - public async Task ValidateMissionRankRulesAsync(Guid missionId, Guid playerId) - { - try - { - var player = await _uow.Players.GetByIdAsync(playerId); - if (player == null) return false; - var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync(); - if (rankRules.Count == 0) return true; // no restriction - return rankRules.Contains(player.RankId); - } - catch (Exception ex) { Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } - } - - public async Task ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId) - { - try - { - var player = await _uow.Players.GetByIdAsync(playerId); - if (player == null) return false; - var rank = await _uow.Ranks.GetByIdAsync(targetRankId); - if (rank == null) return false; - if (player.Experience < rank.ExpNeeded) return false; - // required missions - var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync(); - if (missionReqs.Count > 0) - { - var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync(); - if (missionReqs.Except(completed).Any()) return false; - } - // required skills - var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync(); - if (skillReqs.Count > 0) - { - var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); - foreach (var req in skillReqs) - { - var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId); - if (ps == null || ps.Score < req.Min) return false; - } - } - return true; - } - catch (Exception ex) { Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId); throw; } - } - - public async Task> GetApplicableRankRulesAsync(Guid missionId) - { - try { return await _uow.MissionRankRules.Query(r => r.MissionId == missionId, null, r => r.Rank).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetApplicableRankRulesAsync failed {MissionId}", missionId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class RuleValidationService : IRuleValidationService +{ + private readonly IUnitOfWork _uow; + public RuleValidationService(IUnitOfWork uow) => _uow = uow; + + public async Task ValidateMissionRankRulesAsync(Guid missionId, Guid playerId) + { + try + { + var player = await _uow.Players.GetByIdAsync(playerId); + if (player == null) return false; + var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync(); + if (rankRules.Count == 0) return true; // no restriction + return rankRules.Contains(player.RankId); + } + catch (Exception ex) { Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; } + } + + public async Task ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId) + { + try + { + var player = await _uow.Players.GetByIdAsync(playerId); + if (player == null) return false; + var rank = await _uow.Ranks.GetByIdAsync(targetRankId); + if (rank == null) return false; + if (player.Experience < rank.ExpNeeded) return false; + // required missions + var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync(); + if (missionReqs.Count > 0) + { + var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync(); + if (missionReqs.Except(completed).Any()) return false; + } + // required skills + var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync(); + if (skillReqs.Count > 0) + { + var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync(); + foreach (var req in skillReqs) + { + var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId); + if (ps == null || ps.Score < req.Min) return false; + } + } + return true; + } + catch (Exception ex) { Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId); throw; } + } + + public async Task> GetApplicableRankRulesAsync(Guid missionId) + { + try { return await _uow.MissionRankRules.Query(r => r.MissionId == missionId, null, r => r.Rank).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetApplicableRankRulesAsync failed {MissionId}", missionId); throw; } + } +} diff --git a/LctMonolith/Services/S3FileStorageService.cs b/LctMonolith/Services/S3FileStorageService.cs index 375e0ce..bbbcba9 100644 --- a/LctMonolith/Services/S3FileStorageService.cs +++ b/LctMonolith/Services/S3FileStorageService.cs @@ -1,102 +1,102 @@ -using Amazon.S3; -using Amazon.S3.Model; -using Amazon; -using Microsoft.Extensions.Options; -using LctMonolith.Application.Options; -using LctMonolith.Services.Interfaces; -using Serilog; - -namespace LctMonolith.Services; - -public class S3FileStorageService : IFileStorageService, IDisposable -{ - private readonly S3StorageOptions _opts; - private readonly IAmazonS3 _client; - private bool _bucketChecked; - - public S3FileStorageService(IOptions options) - { - _opts = options.Value; - var cfg = new AmazonS3Config - { - ServiceURL = _opts.Endpoint, - ForcePathStyle = true, - UseHttp = !_opts.UseSsl, - Timeout = TimeSpan.FromSeconds(30), - MaxErrorRetry = 2, - }; - _client = new AmazonS3Client(_opts.AccessKey, _opts.SecretKey, cfg); - } - - private async Task EnsureBucketAsync(CancellationToken ct) - { - if (_bucketChecked) return; - try - { - var list = await _client.ListBucketsAsync(ct); - if (!list.Buckets.Any(b => string.Equals(b.BucketName, _opts.Bucket, StringComparison.OrdinalIgnoreCase))) - { - await _client.PutBucketAsync(new PutBucketRequest { BucketName = _opts.Bucket }, ct); - } - _bucketChecked = true; - } - catch (Exception ex) - { - Log.Error(ex, "Failed ensuring bucket {Bucket}", _opts.Bucket); - throw; - } - } - - public async Task UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default) - { - await EnsureBucketAsync(ct); - var key = $"{keyPrefix.Trim('/')}/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}"; - var putReq = new PutObjectRequest - { - BucketName = _opts.Bucket, - Key = key, - InputStream = content, - ContentType = contentType - }; - await _client.PutObjectAsync(putReq, ct); - Log.Information("Uploaded object {Key} to bucket {Bucket}", key, _opts.Bucket); - return key; - } - - public async Task DeleteAsync(string key, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(key)) return; - try - { - await _client.DeleteObjectAsync(_opts.Bucket, key, ct); - Log.Information("Deleted object {Key}", key); - } - catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - // ignore - } - } - - public Task GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default) - { - if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key)); - if (!string.IsNullOrWhiteSpace(_opts.PublicBaseUrl)) - { - var url = _opts.PublicBaseUrl!.TrimEnd('/') + "/" + key; - return Task.FromResult(url); - } - var req = new GetPreSignedUrlRequest - { - BucketName = _opts.Bucket, - Key = key, - Expires = DateTime.UtcNow.Add(expires ?? TimeSpan.FromMinutes(_opts.PresignExpirationMinutes)) - }; - var urlSigned = _client.GetPreSignedURL(req); - return Task.FromResult(urlSigned); - } - - public void Dispose() - { - _client.Dispose(); - } -} +using Amazon.S3; +using Amazon.S3.Model; +using Amazon; +using Microsoft.Extensions.Options; +using LctMonolith.Application.Options; +using LctMonolith.Services.Interfaces; +using Serilog; + +namespace LctMonolith.Services; + +public class S3FileStorageService : IFileStorageService, IDisposable +{ + private readonly S3StorageOptions _opts; + private readonly IAmazonS3 _client; + private bool _bucketChecked; + + public S3FileStorageService(IOptions options) + { + _opts = options.Value; + var cfg = new AmazonS3Config + { + ServiceURL = _opts.Endpoint, + ForcePathStyle = true, + UseHttp = !_opts.UseSsl, + Timeout = TimeSpan.FromSeconds(30), + MaxErrorRetry = 2, + }; + _client = new AmazonS3Client(_opts.AccessKey, _opts.SecretKey, cfg); + } + + private async Task EnsureBucketAsync(CancellationToken ct) + { + if (_bucketChecked) return; + try + { + var list = await _client.ListBucketsAsync(ct); + if (!list.Buckets.Any(b => string.Equals(b.BucketName, _opts.Bucket, StringComparison.OrdinalIgnoreCase))) + { + await _client.PutBucketAsync(new PutBucketRequest { BucketName = _opts.Bucket }, ct); + } + _bucketChecked = true; + } + catch (Exception ex) + { + Log.Error(ex, "Failed ensuring bucket {Bucket}", _opts.Bucket); + throw; + } + } + + public async Task UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default) + { + await EnsureBucketAsync(ct); + var key = $"{keyPrefix.Trim('/')}/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}"; + var putReq = new PutObjectRequest + { + BucketName = _opts.Bucket, + Key = key, + InputStream = content, + ContentType = contentType + }; + await _client.PutObjectAsync(putReq, ct); + Log.Information("Uploaded object {Key} to bucket {Bucket}", key, _opts.Bucket); + return key; + } + + public async Task DeleteAsync(string key, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(key)) return; + try + { + await _client.DeleteObjectAsync(_opts.Bucket, key, ct); + Log.Information("Deleted object {Key}", key); + } + catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + // ignore + } + } + + public Task GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key)); + if (!string.IsNullOrWhiteSpace(_opts.PublicBaseUrl)) + { + var url = _opts.PublicBaseUrl!.TrimEnd('/') + "/" + key; + return Task.FromResult(url); + } + var req = new GetPreSignedUrlRequest + { + BucketName = _opts.Bucket, + Key = key, + Expires = DateTime.UtcNow.Add(expires ?? TimeSpan.FromMinutes(_opts.PresignExpirationMinutes)) + }; + var urlSigned = _client.GetPreSignedURL(req); + return Task.FromResult(urlSigned); + } + + public void Dispose() + { + _client.Dispose(); + } +} diff --git a/LctMonolith/Services/SkillService.cs b/LctMonolith/Services/SkillService.cs index 70363b2..7296d0a 100644 --- a/LctMonolith/Services/SkillService.cs +++ b/LctMonolith/Services/SkillService.cs @@ -1,59 +1,59 @@ -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Interfaces; -using Microsoft.EntityFrameworkCore; -using Serilog; - -namespace LctMonolith.Services; - -public class SkillService : ISkillService -{ - private readonly IUnitOfWork _uow; - public SkillService(IUnitOfWork uow) => _uow = uow; - - public async Task GetSkillByIdAsync(Guid skillId) - { - try { return await _uow.Skills.GetByIdAsync(skillId); } - catch (Exception ex) { Log.Error(ex, "GetSkillByIdAsync failed {SkillId}", skillId); throw; } - } - public async Task GetSkillByTitleAsync(string title) - { - try { return await _uow.Skills.Query(s => s.Title == title).FirstOrDefaultAsync(); } - catch (Exception ex) { Log.Error(ex, "GetSkillByTitleAsync failed {Title}", title); throw; } - } - public async Task> GetAllSkillsAsync() - { - try { return await _uow.Skills.Query().OrderBy(s => s.Title).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetAllSkillsAsync failed"); throw; } - } - public async Task CreateSkillAsync(Skill skill) - { - try { skill.Id = Guid.NewGuid(); await _uow.Skills.AddAsync(skill); await _uow.SaveChangesAsync(); return skill; } - catch (Exception ex) { Log.Error(ex, "CreateSkillAsync failed {Title}", skill.Title); throw; } - } - public async Task UpdateSkillAsync(Skill skill) - { - try { _uow.Skills.Update(skill); await _uow.SaveChangesAsync(); return skill; } - catch (Exception ex) { Log.Error(ex, "UpdateSkillAsync failed {SkillId}", skill.Id); throw; } - } - public async Task DeleteSkillAsync(Guid skillId) - { - try { var skill = await _uow.Skills.GetByIdAsync(skillId); if (skill == null) return false; _uow.Skills.Remove(skill); await _uow.SaveChangesAsync(); return true; } - catch (Exception ex) { Log.Error(ex, "DeleteSkillAsync failed {SkillId}", skillId); throw; } - } - public async Task UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level) - { - try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); if (ps == null) { ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = skillId, Score = level }; await _uow.PlayerSkills.AddAsync(ps); } else { ps.Score = level; _uow.PlayerSkills.Update(ps); } await _uow.SaveChangesAsync(); return ps; } - catch (Exception ex) { Log.Error(ex, "UpdatePlayerSkillAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; } - } - public async Task> GetPlayerSkillsAsync(Guid playerId) - { - try { return await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill).ToListAsync(); } - catch (Exception ex) { Log.Error(ex, "GetPlayerSkillsAsync failed {PlayerId}", playerId); throw; } - } - public async Task GetPlayerSkillLevelAsync(Guid playerId, Guid skillId) - { - try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); return ps?.Score ?? 0; } - catch (Exception ex) { Log.Error(ex, "GetPlayerSkillLevelAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; } - } -} +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace LctMonolith.Services; + +public class SkillService : ISkillService +{ + private readonly IUnitOfWork _uow; + public SkillService(IUnitOfWork uow) => _uow = uow; + + public async Task GetSkillByIdAsync(Guid skillId) + { + try { return await _uow.Skills.GetByIdAsync(skillId); } + catch (Exception ex) { Log.Error(ex, "GetSkillByIdAsync failed {SkillId}", skillId); throw; } + } + public async Task GetSkillByTitleAsync(string title) + { + try { return await _uow.Skills.Query(s => s.Title == title).FirstOrDefaultAsync(); } + catch (Exception ex) { Log.Error(ex, "GetSkillByTitleAsync failed {Title}", title); throw; } + } + public async Task> GetAllSkillsAsync() + { + try { return await _uow.Skills.Query().OrderBy(s => s.Title).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetAllSkillsAsync failed"); throw; } + } + public async Task CreateSkillAsync(Skill skill) + { + try { skill.Id = Guid.NewGuid(); await _uow.Skills.AddAsync(skill); await _uow.SaveChangesAsync(); return skill; } + catch (Exception ex) { Log.Error(ex, "CreateSkillAsync failed {Title}", skill.Title); throw; } + } + public async Task UpdateSkillAsync(Skill skill) + { + try { _uow.Skills.Update(skill); await _uow.SaveChangesAsync(); return skill; } + catch (Exception ex) { Log.Error(ex, "UpdateSkillAsync failed {SkillId}", skill.Id); throw; } + } + public async Task DeleteSkillAsync(Guid skillId) + { + try { var skill = await _uow.Skills.GetByIdAsync(skillId); if (skill == null) return false; _uow.Skills.Remove(skill); await _uow.SaveChangesAsync(); return true; } + catch (Exception ex) { Log.Error(ex, "DeleteSkillAsync failed {SkillId}", skillId); throw; } + } + public async Task UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level) + { + try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); if (ps == null) { ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = skillId, Score = level }; await _uow.PlayerSkills.AddAsync(ps); } else { ps.Score = level; _uow.PlayerSkills.Update(ps); } await _uow.SaveChangesAsync(); return ps; } + catch (Exception ex) { Log.Error(ex, "UpdatePlayerSkillAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; } + } + public async Task> GetPlayerSkillsAsync(Guid playerId) + { + try { return await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill).ToListAsync(); } + catch (Exception ex) { Log.Error(ex, "GetPlayerSkillsAsync failed {PlayerId}", playerId); throw; } + } + public async Task GetPlayerSkillLevelAsync(Guid playerId, Guid skillId) + { + try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); return ps?.Score ?? 0; } + catch (Exception ex) { Log.Error(ex, "GetPlayerSkillLevelAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; } + } +} diff --git a/LctMonolith/Services/StoreService.cs b/LctMonolith/Services/StoreService.cs index 7191a3a..3f15a55 100644 --- a/LctMonolith/Services/StoreService.cs +++ b/LctMonolith/Services/StoreService.cs @@ -1,62 +1,62 @@ -using LctMonolith.Models.Database; -using Microsoft.EntityFrameworkCore; -using Serilog; -using System.Text.Json; -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Services.Contracts; - -namespace LctMonolith.Services; - -/// -/// Store purchase operations and inventory management. -/// -public class StoreService : IStoreService -{ - private readonly IUnitOfWork _uow; - - public StoreService(IUnitOfWork uow) - { - _uow = uow; - } - - public async Task> GetActiveItemsAsync(CancellationToken ct = default) - { - try { return await _uow.StoreItems.Query(i => i.IsActive).ToListAsync(ct); } - catch (Exception ex) { Log.Error(ex, "GetActiveItemsAsync failed"); throw; } - } - - public async Task PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default) - { - try - { - if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity)); - var player = await _uow.Players.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Player not found for user"); - var item = await _uow.StoreItems.Query(i => i.Id == itemId && i.IsActive).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Item not found or inactive"); - var totalPrice = item.Price * quantity; - if (player.Mana < totalPrice) throw new InvalidOperationException("Insufficient mana"); - if (item.Stock.HasValue && item.Stock.Value < quantity) throw new InvalidOperationException("Insufficient stock"); - - player.Mana -= totalPrice; - if (item.Stock.HasValue) item.Stock -= quantity; - - var inv = await _uow.UserInventoryItems.FindAsync(userId, itemId); - if (inv == null) - { - inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow }; - await _uow.UserInventoryItems.AddAsync(inv, ct); - } - else inv.Quantity += quantity; - - await _uow.Transactions.AddAsync(new Transaction { UserId = userId, StoreItemId = itemId, Type = TransactionType.Purchase, ManaAmount = -totalPrice }, ct); - await _uow.EventLogs.AddAsync(new EventLog { Type = EventType.ItemPurchased, UserId = userId, Data = JsonSerializer.Serialize(new { itemId, quantity, totalPrice }) }, ct); - await _uow.SaveChangesAsync(ct); - Log.Information("User {User} purchased {Qty} of {Item}", userId, quantity, itemId); - return inv; - } - catch (Exception ex) - { - Log.Error(ex, "PurchaseAsync failed {UserId} {ItemId}", userId, itemId); - throw; - } - } -} +using LctMonolith.Models.Database; +using Microsoft.EntityFrameworkCore; +using Serilog; +using System.Text.Json; +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Services.Contracts; + +namespace LctMonolith.Services; + +/// +/// Store purchase operations and inventory management. +/// +public class StoreService : IStoreService +{ + private readonly IUnitOfWork _uow; + + public StoreService(IUnitOfWork uow) + { + _uow = uow; + } + + public async Task> GetActiveItemsAsync(CancellationToken ct = default) + { + try { return await _uow.StoreItems.Query(i => i.IsActive).ToListAsync(ct); } + catch (Exception ex) { Log.Error(ex, "GetActiveItemsAsync failed"); throw; } + } + + public async Task PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default) + { + try + { + if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity)); + var player = await _uow.Players.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Player not found for user"); + var item = await _uow.StoreItems.Query(i => i.Id == itemId && i.IsActive).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Item not found or inactive"); + var totalPrice = item.Price * quantity; + if (player.Mana < totalPrice) throw new InvalidOperationException("Insufficient mana"); + if (item.Stock.HasValue && item.Stock.Value < quantity) throw new InvalidOperationException("Insufficient stock"); + + player.Mana -= totalPrice; + if (item.Stock.HasValue) item.Stock -= quantity; + + var inv = await _uow.UserInventoryItems.FindAsync(userId, itemId); + if (inv == null) + { + inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow }; + await _uow.UserInventoryItems.AddAsync(inv, ct); + } + else inv.Quantity += quantity; + + await _uow.Transactions.AddAsync(new Transaction { UserId = userId, StoreItemId = itemId, Type = TransactionType.Purchase, ManaAmount = -totalPrice }, ct); + await _uow.EventLogs.AddAsync(new EventLog { Type = EventType.ItemPurchased, UserId = userId, Data = JsonSerializer.Serialize(new { itemId, quantity, totalPrice }) }, ct); + await _uow.SaveChangesAsync(ct); + Log.Information("User {User} purchased {Qty} of {Item}", userId, quantity, itemId); + return inv; + } + catch (Exception ex) + { + Log.Error(ex, "PurchaseAsync failed {UserId} {ItemId}", userId, itemId); + throw; + } + } +} diff --git a/LctMonolith/Services/TokenService.cs b/LctMonolith/Services/TokenService.cs index ec2e1ca..7f146fd 100644 --- a/LctMonolith/Services/TokenService.cs +++ b/LctMonolith/Services/TokenService.cs @@ -1,123 +1,123 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using LctMonolith.Application.Options; -using LctMonolith.Database.UnitOfWork; -using LctMonolith.Models.Database; -using LctMonolith.Services.Contracts; -using LctMonolith.Services.Models; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Serilog; - -namespace LctMonolith.Services; - -/// -/// Issues and refreshes JWT + refresh tokens. -/// -public class TokenService : ITokenService -{ - private readonly IUnitOfWork _uow; - private readonly UserManager _userManager; - private readonly JwtOptions _options; - private readonly SigningCredentials _creds; - - public TokenService(IUnitOfWork uow, UserManager userManager, IOptions options) - { - _uow = uow; - _userManager = userManager; - _options = options.Value; - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key)); - _creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - } - - public async Task IssueAsync(AppUser user, CancellationToken ct = default) - { - try - { - var now = DateTime.UtcNow; - var accessExp = now.AddMinutes(_options.AccessTokenMinutes); - var refreshExp = now.AddDays(_options.RefreshTokenDays); - var claims = new List - { - new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), - new(ClaimTypes.NameIdentifier, user.Id.ToString()), - new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty), - new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - }; - var jwt = new JwtSecurityToken( - issuer: _options.Issuer, - audience: _options.Audience, - claims: claims, - notBefore: now, - expires: accessExp, - signingCredentials: _creds); - var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt); - - var refreshToken = GenerateSecureToken(); - var rt = new RefreshToken - { - Token = refreshToken, - UserId = user.Id, - ExpiresAt = refreshExp - }; - await _uow.RefreshTokens.AddAsync(rt, ct); - await _uow.SaveChangesAsync(ct); - return new TokenPair(accessToken, accessExp, refreshToken, refreshExp); - } - catch (Exception ex) - { - Log.Error(ex, "IssueAsync failed for user {UserId}", user.Id); - throw; - } - } - - public async Task RefreshAsync(string refreshToken, CancellationToken ct = default) - { - try - { - var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct); - if (token == null || token.IsRevoked || token.ExpiresAt < DateTime.UtcNow) - throw new SecurityTokenException("Invalid refresh token"); - var user = await _userManager.FindByIdAsync(token.UserId.ToString()) ?? throw new SecurityTokenException("User not found"); - token.IsRevoked = true; // rotate - await _uow.SaveChangesAsync(ct); - return await IssueAsync(user, ct); - } - catch (Exception ex) - { - Log.Error(ex, "RefreshAsync failed"); - throw; - } - } - - public async Task RevokeAsync(string refreshToken, CancellationToken ct = default) - { - try - { - var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct); - if (token == null) return; // idempotent - token.IsRevoked = true; - await _uow.SaveChangesAsync(ct); - } - catch (Exception ex) - { - Log.Error(ex, "RevokeAsync failed"); - throw; - } - } - - private static string GenerateSecureToken() - { - Span bytes = stackalloc byte[64]; - RandomNumberGenerator.Fill(bytes); - return Convert.ToBase64String(bytes); - } -} - -internal static class EfAsyncExtensions -{ - public static Task FirstOrDefaultAsync(this IQueryable query, CancellationToken ct = default) => Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(query, ct); -} +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using LctMonolith.Application.Options; +using LctMonolith.Database.UnitOfWork; +using LctMonolith.Models.Database; +using LctMonolith.Services.Contracts; +using LctMonolith.Services.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Serilog; + +namespace LctMonolith.Services; + +/// +/// Issues and refreshes JWT + refresh tokens. +/// +public class TokenService : ITokenService +{ + private readonly IUnitOfWork _uow; + private readonly UserManager _userManager; + private readonly JwtOptions _options; + private readonly SigningCredentials _creds; + + public TokenService(IUnitOfWork uow, UserManager userManager, IOptions options) + { + _uow = uow; + _userManager = userManager; + _options = options.Value; + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key)); + _creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + } + + public async Task IssueAsync(AppUser user, CancellationToken ct = default) + { + try + { + var now = DateTime.UtcNow; + var accessExp = now.AddMinutes(_options.AccessTokenMinutes); + var refreshExp = now.AddDays(_options.RefreshTokenDays); + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new(ClaimTypes.NameIdentifier, user.Id.ToString()), + new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + var jwt = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Audience, + claims: claims, + notBefore: now, + expires: accessExp, + signingCredentials: _creds); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt); + + var refreshToken = GenerateSecureToken(); + var rt = new RefreshToken + { + Token = refreshToken, + UserId = user.Id, + ExpiresAt = refreshExp + }; + await _uow.RefreshTokens.AddAsync(rt, ct); + await _uow.SaveChangesAsync(ct); + return new TokenPair(accessToken, accessExp, refreshToken, refreshExp); + } + catch (Exception ex) + { + Log.Error(ex, "IssueAsync failed for user {UserId}", user.Id); + throw; + } + } + + public async Task RefreshAsync(string refreshToken, CancellationToken ct = default) + { + try + { + var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct); + if (token == null || token.IsRevoked || token.ExpiresAt < DateTime.UtcNow) + throw new SecurityTokenException("Invalid refresh token"); + var user = await _userManager.FindByIdAsync(token.UserId.ToString()) ?? throw new SecurityTokenException("User not found"); + token.IsRevoked = true; // rotate + await _uow.SaveChangesAsync(ct); + return await IssueAsync(user, ct); + } + catch (Exception ex) + { + Log.Error(ex, "RefreshAsync failed"); + throw; + } + } + + public async Task RevokeAsync(string refreshToken, CancellationToken ct = default) + { + try + { + var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct); + if (token == null) return; // idempotent + token.IsRevoked = true; + await _uow.SaveChangesAsync(ct); + } + catch (Exception ex) + { + Log.Error(ex, "RevokeAsync failed"); + throw; + } + } + + private static string GenerateSecureToken() + { + Span bytes = stackalloc byte[64]; + RandomNumberGenerator.Fill(bytes); + return Convert.ToBase64String(bytes); + } +} + +internal static class EfAsyncExtensions +{ + public static Task FirstOrDefaultAsync(this IQueryable query, CancellationToken ct = default) => Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(query, ct); +} diff --git a/LctMonolith/appsettings.Development.json b/LctMonolith/appsettings.Development.json index e62cba1..9ff01d6 100644 --- a/LctMonolith/appsettings.Development.json +++ b/LctMonolith/appsettings.Development.json @@ -1,35 +1,35 @@ -{ - "ConnectionStrings": { - "Default": "Host=localhost;Port=5432;Database=lct2025_dev;Username=postgres;Password=postgres" - }, - "Jwt": { - "Key": "Dev_Insecure_Key_Change_Me", - "Issuer": "LctMonolith", - "Audience": "LctMonolithAudience", - "AccessTokenMinutes": 120, - "RefreshTokenDays": 7 - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], - "MinimumLevel": { - "Default": "Debug", - "Override": { - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "WriteTo": [ - { "Name": "Console" }, - { "Name": "Debug" }, - { "Name": "File", "Args": { "path": "Logs/dev-log-.txt", "rollingInterval": "Day", "shared": true } } - ], - "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], - "Properties": { "Application": "LctMonolith" } - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} +{ + "ConnectionStrings": { + "Default": "Host=localhost;Port=5432;Database=lct2025_dev;Username=postgres;Password=postgres" + }, + "Jwt": { + "Key": "Dev_Insecure_Key_Change_Me", + "Issuer": "LctMonolith", + "Audience": "LctMonolithAudience", + "AccessTokenMinutes": 120, + "RefreshTokenDays": 7 + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" }, + { "Name": "Debug" }, + { "Name": "File", "Args": { "path": "Logs/dev-log-.txt", "rollingInterval": "Day", "shared": true } } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "Properties": { "Application": "LctMonolith" } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LctMonolith/appsettings.json b/LctMonolith/appsettings.json index c30e24e..de6f563 100644 --- a/LctMonolith/appsettings.json +++ b/LctMonolith/appsettings.json @@ -1,36 +1,36 @@ -{ - "ConnectionStrings": { - "Default": "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres" - }, - "Jwt": { - "Key": "Dev_Insecure_Key_Change_Me", - "Issuer": "LctMonolith", - "Audience": "LctMonolithAudience", - "AccessTokenMinutes": 60, - "RefreshTokenDays": 7 - }, - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "WriteTo": [ - { "Name": "Console" }, - { "Name": "Debug" }, - { "Name": "File", "Args": { "path": "Logs/log-.txt", "rollingInterval": "Day", "shared": true } } - ], - "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], - "Properties": { "Application": "LctMonolith" } - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "ConnectionStrings": { + "Default": "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres" + }, + "Jwt": { + "Key": "Dev_Insecure_Key_Change_Me", + "Issuer": "LctMonolith", + "Audience": "LctMonolithAudience", + "AccessTokenMinutes": 60, + "RefreshTokenDays": 7 + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" }, + { "Name": "Debug" }, + { "Name": "File", "Args": { "path": "Logs/log-.txt", "rollingInterval": "Day", "shared": true } } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "Properties": { "Application": "LctMonolith" } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/LctMonolith/openapi-gamification.yaml b/LctMonolith/openapi-gamification.yaml index 46d6a23..a0ad5c6 100644 --- a/LctMonolith/openapi-gamification.yaml +++ b/LctMonolith/openapi-gamification.yaml @@ -1,927 +1,927 @@ -openapi: 3.0.3 -info: - title: LctMonolith Gamification API - version: 1.0.0 - description: | - Comprehensive REST API for gamification module (players, missions, ranks, skills, rewards, dialogue, inventory, store, analytics, notifications, auth). - Authentication via JWT Bearer (Authorization: Bearer ). Admin endpoints require role=Admin. -servers: - - url: https://localhost:5001 - description: Local HTTPS - - url: http://localhost:5000 - description: Local HTTP -security: - - bearerAuth: [] -tags: - - name: Auth - - name: Players - - name: Ranks - - name: Skills - - name: MissionCategories - - name: Missions - - name: Rewards - - name: Dialogue - - name: Inventory - - name: Store - - name: Notifications - - name: Analytics - - name: Profile -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - schemas: - Id: { type: string, format: uuid } - TokenPair: - type: object - properties: - accessToken: { type: string } - refreshToken: { type: string } - expiresAt: { type: string, format: date-time } - refreshExpiresAt: { type: string, format: date-time, nullable: true } - Rank: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - title: { type: string } - expNeeded: { type: integer } - Skill: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - title: { type: string } - Player: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - userId: { $ref: '#/components/schemas/Id' } - rankId: { $ref: '#/components/schemas/Id' } - experience: { type: integer } - mana: { type: integer } - PlayerSkill: - type: object - properties: - playerId: { $ref: '#/components/schemas/Id' } - skillId: { $ref: '#/components/schemas/Id' } - score: { type: integer } - Mission: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - title: { type: string } - description: { type: string } - missionCategoryId: { $ref: '#/components/schemas/Id' } - parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } - expReward: { type: integer } - manaReward: { type: integer } - MissionSkillReward: - type: object - properties: - missionId: { $ref: '#/components/schemas/Id' } - skillId: { $ref: '#/components/schemas/Id' } - value: { type: integer } - MissionItemReward: - type: object - properties: - missionId: { $ref: '#/components/schemas/Id' } - itemId: { $ref: '#/components/schemas/Id' } - SkillProgress: - type: object - properties: - skillId: { $ref: '#/components/schemas/Id' } - skillTitle: { type: string } - previousLevel: { type: integer } - newLevel: { type: integer } - MissionCompletionResult: - type: object - properties: - success: { type: boolean } - message: { type: string } - experienceGained: { type: integer } - manaGained: { type: integer } - skillsProgress: { type: array, items: { $ref: '#/components/schemas/SkillProgress' } } - unlockedMissions: { type: array, items: { $ref: '#/components/schemas/Id' } } - PlayerProgress: - type: object - properties: - playerId: { $ref: '#/components/schemas/Id' } - playerName: { type: string } - currentRank: { $ref: '#/components/schemas/Rank', nullable: true } - totalExperience: { type: integer } - totalMana: { type: integer } - completedMissions: { type: integer } - totalAvailableMissions: { type: integer } - skillLevels: - type: object - additionalProperties: { type: integer } - StoreItem: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - name: { type: string } - description: { type: string, nullable: true } - price: { type: integer } - isActive: { type: boolean } - stock: { type: integer, nullable: true } - UserInventoryItem: - type: object - properties: - userId: { $ref: '#/components/schemas/Id' } - storeItemId: { $ref: '#/components/schemas/Id' } - quantity: { type: integer } - acquiredAt: { type: string, format: date-time } - Notification: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - type: { type: string } - title: { type: string } - message: { type: string } - isRead: { type: boolean } - createdAt: { type: string, format: date-time } - readAt: { type: string, format: date-time, nullable: true } - AnalyticsSummary: - type: object - properties: - totalUsers: { type: integer } - totalMissions: { type: integer } - totalStoreItems: { type: integer } - totalExperience: { type: integer } - completedMissions: { type: integer } - generatedAtUtc: { type: string, format: date-time } - Profile: - type: object - properties: - id: { $ref: '#/components/schemas/Id' } - userId: { $ref: '#/components/schemas/Id' } - firstName: { type: string, nullable: true } - lastName: { type: string, nullable: true } - birthDate: { type: string, format: date, nullable: true } - about: { type: string, nullable: true } - location: { type: string, nullable: true } - avatarUrl: { type: string, nullable: true } - createdAt: { type: string, format: date-time } - updatedAt: { type: string, format: date-time } -paths: - /api/auth/register: - post: - tags: [Auth] - summary: Register user - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - email: { type: string, format: email } - password: { type: string } - firstName: { type: string } - lastName: { type: string } - required: [email,password] - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } - /api/auth/login: - post: - tags: [Auth] - summary: Login - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - email: { type: string, format: email } - password: { type: string } - required: [email,password] - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } - /api/auth/refresh: - post: - tags: [Auth] - summary: Refresh access token - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - refreshToken: { type: string } - required: [refreshToken] - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } - /api/auth/revoke: - post: - tags: [Auth] - security: [ { bearerAuth: [] } ] - summary: Revoke refresh token - requestBody: - content: - application/json: - schema: - type: object - properties: - refreshToken: { type: string } - responses: - '204': { description: No Content } - /api/auth/me: - get: - tags: [Auth] - security: [ { bearerAuth: [] } ] - summary: Current user id - responses: - '200': { description: OK } - /api/analytics/summary: - get: - tags: [Analytics] - security: [ { bearerAuth: [] } ] - summary: Aggregated analytics summary - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AnalyticsSummary' } } } } - /api/players/{playerId}: - get: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Get player by id - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK } - '404': { description: Not Found } - /api/players/{playerId}/progress: - get: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Get player overall progress - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/PlayerProgress' } } } } - '404': { description: Not Found } - /api/players/user/{userId}: - get: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Get player by user id - parameters: - - in: path - name: userId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK } - '404': { description: Not Found } - /api/players: - post: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Create player (Admin) - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - userId: { $ref: '#/components/schemas/Id' } - username: { type: string } - required: [userId,username] - responses: - '201': { description: Created } - x-roles: [Admin] - /api/players/{playerId}/experience: - post: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Adjust player experience (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - value: { type: integer } - required: [value] - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK } - x-roles: [Admin] - /api/players/{playerId}/mana: - post: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Adjust player mana (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - value: { type: integer } - required: [value] - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK } - x-roles: [Admin] - /api/players/top: - get: - tags: [Players] - security: [ { bearerAuth: [] } ] - summary: Top players by experience - parameters: - - in: query - name: count - schema: { type: integer, default: 10 } - responses: - '200': { description: OK } - /api/ranks: - get: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: List ranks - responses: { '200': { description: OK } } - post: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: Create rank (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - expNeeded: { type: integer } - required: [title,expNeeded] - responses: { '201': { description: Created } } - x-roles: [Admin] - /api/ranks/{id}: - get: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: Get rank - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - put: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: Update rank (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - expNeeded: { type: integer } - required: [title,expNeeded] - responses: { '200': { description: OK }, '404': { description: Not Found } } - x-roles: [Admin] - delete: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: Delete rank (Admin) - responses: { '204': { description: No Content }, '404': { description: Not Found } } - x-roles: [Admin] - /api/ranks/validate-advance/{playerId}/{targetRankId}: - get: - tags: [Ranks] - security: [ { bearerAuth: [] } ] - summary: Validate advancement - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - - in: path - name: targetRankId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/skills: - get: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: List skills - responses: { '200': { description: OK } } - post: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: Create skill (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - required: [title] - responses: { '201': { description: Created } } - x-roles: [Admin] - /api/skills/{id}: - get: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: Get skill - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - put: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: Update skill (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - required: [title] - responses: { '200': { description: OK }, '404': { description: Not Found } } - x-roles: [Admin] - delete: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: Delete skill (Admin) - responses: { '204': { description: No Content }, '404': { description: Not Found } } - x-roles: [Admin] - /api/skills/player/{playerId}: - get: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: List player skills - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/skills/player/{playerId}/{skillId}: - post: - tags: [Skills] - security: [ { bearerAuth: [] } ] - summary: Update player skill (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - level: { type: integer } - required: [level] - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - - in: path - name: skillId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - x-roles: [Admin] - /api/mission-categories: - get: - tags: [MissionCategories] - security: [ { bearerAuth: [] } ] - summary: List mission categories - responses: { '200': { description: OK } } - post: - tags: [MissionCategories] - security: [ { bearerAuth: [] } ] - summary: Create mission category (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - required: [title] - responses: { '201': { description: Created } } - x-roles: [Admin] - /api/mission-categories/{id}: - get: - tags: [MissionCategories] - security: [ { bearerAuth: [] } ] - summary: Get mission category - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - put: - tags: [MissionCategories] - security: [ { bearerAuth: [] } ] - summary: Update mission category (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - required: [title] - responses: { '200': { description: OK }, '404': { description: Not Found } } - x-roles: [Admin] - delete: - tags: [MissionCategories] - security: [ { bearerAuth: [] } ] - summary: Delete mission category (Admin) - responses: { '204': { description: No Content }, '404': { description: Not Found } } - x-roles: [Admin] - /api/missions/{id}: - get: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Get mission - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - put: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Update mission (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - description: { type: string } - missionCategoryId: { $ref: '#/components/schemas/Id' } - parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } - expReward: { type: integer } - manaReward: { type: integer } - required: [title,missionCategoryId,expReward,manaReward] - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - x-roles: [Admin] - delete: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Delete mission (Admin) - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '204': { description: No Content }, '404': { description: Not Found } } - x-roles: [Admin] - /api/missions/category/{categoryId}: - get: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Missions by category - parameters: - - in: path - name: categoryId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/missions/player/{playerId}/available: - get: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Available missions for player - parameters: - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/missions/{missionId}/rank-rules: - get: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Rank rules for mission - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/missions: - post: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Create mission (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - description: { type: string } - missionCategoryId: { $ref: '#/components/schemas/Id' } - parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } - expReward: { type: integer } - manaReward: { type: integer } - required: [title,missionCategoryId,expReward,manaReward] - responses: { '201': { description: Created } } - x-roles: [Admin] - /api/missions/{id}: - put: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Update mission (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - title: { type: string } - description: { type: string } - missionCategoryId: { $ref: '#/components/schemas/Id' } - parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } - expReward: { type: integer } - manaReward: { type: integer } - required: [title,missionCategoryId,expReward,manaReward] - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - x-roles: [Admin] - delete: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Delete mission (Admin) - parameters: - - in: path - name: id - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '204': { description: No Content }, '404': { description: Not Found } } - x-roles: [Admin] - /api/missions/{missionId}/complete: - post: - tags: [Missions] - security: [ { bearerAuth: [] } ] - summary: Complete mission - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - requestBody: - content: - application/json: - schema: - type: object - properties: - playerId: { $ref: '#/components/schemas/Id' } - proof: { nullable: true } - required: [playerId] - responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/MissionCompletionResult' } } } }, '400': { description: Bad Request } } - /api/rewards/mission/{missionId}/skills: - get: - tags: [Rewards] - security: [ { bearerAuth: [] } ] - summary: Mission skill rewards - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/rewards/mission/{missionId}/items: - get: - tags: [Rewards] - security: [ { bearerAuth: [] } ] - summary: Mission item rewards - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/rewards/mission/{missionId}/can-claim/{playerId}: - get: - tags: [Rewards] - security: [ { bearerAuth: [] } ] - summary: Can claim mission rewards - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - - in: path - name: playerId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/rewards/mission/{missionId}/claim: - post: - tags: [Rewards] - security: [ { bearerAuth: [] } ] - summary: Claim mission rewards - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - requestBody: - content: - application/json: - schema: - type: object - properties: - playerId: { $ref: '#/components/schemas/Id' } - required: [playerId] - responses: { '200': { description: OK }, '409': { description: Conflict } } - /api/rewards/mission/{missionId}/force-distribute: - post: - tags: [Rewards] - security: [ { bearerAuth: [] } ] - summary: Force distribute mission rewards (Admin) - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - requestBody: - content: - application/json: - schema: - type: object - properties: - playerId: { $ref: '#/components/schemas/Id' } - required: [playerId] - responses: { '200': { description: OK } } - x-roles: [Admin] - /api/dialogue/mission/{missionId}: - get: - tags: [Dialogue] - security: [ { bearerAuth: [] } ] - summary: Get dialogue by mission - parameters: - - in: path - name: missionId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - /api/dialogue/message/{messageId}: - get: - tags: [Dialogue] - security: [ { bearerAuth: [] } ] - summary: Get dialogue message - parameters: - - in: path - name: messageId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK }, '404': { description: Not Found } } - /api/dialogue/message/{messageId}/options: - get: - tags: [Dialogue] - security: [ { bearerAuth: [] } ] - summary: Get dialogue response options - parameters: - - in: path - name: messageId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: { '200': { description: OK } } - /api/dialogue/message/{messageId}/respond: - post: - tags: [Dialogue] - security: [ { bearerAuth: [] } ] - summary: Respond to dialogue - parameters: - - in: path - name: messageId - required: true - schema: { $ref: '#/components/schemas/Id' } - requestBody: - content: - application/json: - schema: - type: object - properties: - responseOptionId: { $ref: '#/components/schemas/Id' } - playerId: { $ref: '#/components/schemas/Id' } - required: [responseOptionId,playerId] - responses: { '200': { description: OK } } - /api/dialogue: - post: - tags: [Dialogue] - security: [ { bearerAuth: [] } ] - summary: Create dialogue (Admin) - requestBody: - content: - application/json: - schema: - type: object - properties: - missionId: { $ref: '#/components/schemas/Id' } - initialDialogueMessageId: { $ref: '#/components/schemas/Id' } - interimDialogueMessageId: { $ref: '#/components/schemas/Id' } - endDialogueMessageId: { $ref: '#/components/schemas/Id' } - required: [missionId,initialDialogueMessageId,interimDialogueMessageId,endDialogueMessageId] - responses: { '201': { description: Created } } - x-roles: [Admin] - /api/profile/me: - get: - tags: [Profile] - security: [ { bearerAuth: [] } ] - summary: Get current user profile - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } - '404': { description: Not Found } - /api/profile/{userId}: - get: - tags: [Profile] - security: [ { bearerAuth: [] } ] - summary: Get profile by user id (Admin) - parameters: - - in: path - name: userId - required: true - schema: { $ref: '#/components/schemas/Id' } - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } - '404': { description: Not Found } - x-roles: [Admin] - /api/profile: - put: - tags: [Profile] - security: [ { bearerAuth: [] } ] - summary: Upsert current user profile - requestBody: - content: - application/json: - schema: - type: object - properties: - firstName: { type: string, nullable: true } - lastName: { type: string, nullable: true } - birthDate: { type: string, format: date, nullable: true } - about: { type: string, nullable: true } - location: { type: string, nullable: true } - responses: - '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } - /api/profile/avatar: - post: - tags: [Profile] - security: [ { bearerAuth: [] } ] - summary: Upload avatar image (multipart/form-data) - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - responses: - '200': { description: OK, content: { application/json: { schema: { type: object, properties: { avatarUrl: { type: string } } } } } } - '400': { description: Bad Request } - delete: - tags: [Profile] - security: [ { bearerAuth: [] } ] - summary: Delete avatar image - responses: - '204': { description: No Content } - '404': { description: Not Found } +openapi: 3.0.3 +info: + title: LctMonolith Gamification API + version: 1.0.0 + description: | + Comprehensive REST API for gamification module (players, missions, ranks, skills, rewards, dialogue, inventory, store, analytics, notifications, auth). + Authentication via JWT Bearer (Authorization: Bearer ). Admin endpoints require role=Admin. +servers: + - url: https://localhost:5001 + description: Local HTTPS + - url: http://localhost:5000 + description: Local HTTP +security: + - bearerAuth: [] +tags: + - name: Auth + - name: Players + - name: Ranks + - name: Skills + - name: MissionCategories + - name: Missions + - name: Rewards + - name: Dialogue + - name: Inventory + - name: Store + - name: Notifications + - name: Analytics + - name: Profile +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + Id: { type: string, format: uuid } + TokenPair: + type: object + properties: + accessToken: { type: string } + refreshToken: { type: string } + expiresAt: { type: string, format: date-time } + refreshExpiresAt: { type: string, format: date-time, nullable: true } + Rank: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + title: { type: string } + expNeeded: { type: integer } + Skill: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + title: { type: string } + Player: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + userId: { $ref: '#/components/schemas/Id' } + rankId: { $ref: '#/components/schemas/Id' } + experience: { type: integer } + mana: { type: integer } + PlayerSkill: + type: object + properties: + playerId: { $ref: '#/components/schemas/Id' } + skillId: { $ref: '#/components/schemas/Id' } + score: { type: integer } + Mission: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + title: { type: string } + description: { type: string } + missionCategoryId: { $ref: '#/components/schemas/Id' } + parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } + expReward: { type: integer } + manaReward: { type: integer } + MissionSkillReward: + type: object + properties: + missionId: { $ref: '#/components/schemas/Id' } + skillId: { $ref: '#/components/schemas/Id' } + value: { type: integer } + MissionItemReward: + type: object + properties: + missionId: { $ref: '#/components/schemas/Id' } + itemId: { $ref: '#/components/schemas/Id' } + SkillProgress: + type: object + properties: + skillId: { $ref: '#/components/schemas/Id' } + skillTitle: { type: string } + previousLevel: { type: integer } + newLevel: { type: integer } + MissionCompletionResult: + type: object + properties: + success: { type: boolean } + message: { type: string } + experienceGained: { type: integer } + manaGained: { type: integer } + skillsProgress: { type: array, items: { $ref: '#/components/schemas/SkillProgress' } } + unlockedMissions: { type: array, items: { $ref: '#/components/schemas/Id' } } + PlayerProgress: + type: object + properties: + playerId: { $ref: '#/components/schemas/Id' } + playerName: { type: string } + currentRank: { $ref: '#/components/schemas/Rank', nullable: true } + totalExperience: { type: integer } + totalMana: { type: integer } + completedMissions: { type: integer } + totalAvailableMissions: { type: integer } + skillLevels: + type: object + additionalProperties: { type: integer } + StoreItem: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + name: { type: string } + description: { type: string, nullable: true } + price: { type: integer } + isActive: { type: boolean } + stock: { type: integer, nullable: true } + UserInventoryItem: + type: object + properties: + userId: { $ref: '#/components/schemas/Id' } + storeItemId: { $ref: '#/components/schemas/Id' } + quantity: { type: integer } + acquiredAt: { type: string, format: date-time } + Notification: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + type: { type: string } + title: { type: string } + message: { type: string } + isRead: { type: boolean } + createdAt: { type: string, format: date-time } + readAt: { type: string, format: date-time, nullable: true } + AnalyticsSummary: + type: object + properties: + totalUsers: { type: integer } + totalMissions: { type: integer } + totalStoreItems: { type: integer } + totalExperience: { type: integer } + completedMissions: { type: integer } + generatedAtUtc: { type: string, format: date-time } + Profile: + type: object + properties: + id: { $ref: '#/components/schemas/Id' } + userId: { $ref: '#/components/schemas/Id' } + firstName: { type: string, nullable: true } + lastName: { type: string, nullable: true } + birthDate: { type: string, format: date, nullable: true } + about: { type: string, nullable: true } + location: { type: string, nullable: true } + avatarUrl: { type: string, nullable: true } + createdAt: { type: string, format: date-time } + updatedAt: { type: string, format: date-time } +paths: + /api/auth/register: + post: + tags: [Auth] + summary: Register user + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: { type: string, format: email } + password: { type: string } + firstName: { type: string } + lastName: { type: string } + required: [email,password] + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } + /api/auth/login: + post: + tags: [Auth] + summary: Login + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: { type: string, format: email } + password: { type: string } + required: [email,password] + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } + /api/auth/refresh: + post: + tags: [Auth] + summary: Refresh access token + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + refreshToken: { type: string } + required: [refreshToken] + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } } + /api/auth/revoke: + post: + tags: [Auth] + security: [ { bearerAuth: [] } ] + summary: Revoke refresh token + requestBody: + content: + application/json: + schema: + type: object + properties: + refreshToken: { type: string } + responses: + '204': { description: No Content } + /api/auth/me: + get: + tags: [Auth] + security: [ { bearerAuth: [] } ] + summary: Current user id + responses: + '200': { description: OK } + /api/analytics/summary: + get: + tags: [Analytics] + security: [ { bearerAuth: [] } ] + summary: Aggregated analytics summary + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AnalyticsSummary' } } } } + /api/players/{playerId}: + get: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Get player by id + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK } + '404': { description: Not Found } + /api/players/{playerId}/progress: + get: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Get player overall progress + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/PlayerProgress' } } } } + '404': { description: Not Found } + /api/players/user/{userId}: + get: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Get player by user id + parameters: + - in: path + name: userId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK } + '404': { description: Not Found } + /api/players: + post: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Create player (Admin) + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userId: { $ref: '#/components/schemas/Id' } + username: { type: string } + required: [userId,username] + responses: + '201': { description: Created } + x-roles: [Admin] + /api/players/{playerId}/experience: + post: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Adjust player experience (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + value: { type: integer } + required: [value] + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK } + x-roles: [Admin] + /api/players/{playerId}/mana: + post: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Adjust player mana (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + value: { type: integer } + required: [value] + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK } + x-roles: [Admin] + /api/players/top: + get: + tags: [Players] + security: [ { bearerAuth: [] } ] + summary: Top players by experience + parameters: + - in: query + name: count + schema: { type: integer, default: 10 } + responses: + '200': { description: OK } + /api/ranks: + get: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: List ranks + responses: { '200': { description: OK } } + post: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: Create rank (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + expNeeded: { type: integer } + required: [title,expNeeded] + responses: { '201': { description: Created } } + x-roles: [Admin] + /api/ranks/{id}: + get: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: Get rank + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + put: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: Update rank (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + expNeeded: { type: integer } + required: [title,expNeeded] + responses: { '200': { description: OK }, '404': { description: Not Found } } + x-roles: [Admin] + delete: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: Delete rank (Admin) + responses: { '204': { description: No Content }, '404': { description: Not Found } } + x-roles: [Admin] + /api/ranks/validate-advance/{playerId}/{targetRankId}: + get: + tags: [Ranks] + security: [ { bearerAuth: [] } ] + summary: Validate advancement + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + - in: path + name: targetRankId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/skills: + get: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: List skills + responses: { '200': { description: OK } } + post: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: Create skill (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + required: [title] + responses: { '201': { description: Created } } + x-roles: [Admin] + /api/skills/{id}: + get: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: Get skill + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + put: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: Update skill (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + required: [title] + responses: { '200': { description: OK }, '404': { description: Not Found } } + x-roles: [Admin] + delete: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: Delete skill (Admin) + responses: { '204': { description: No Content }, '404': { description: Not Found } } + x-roles: [Admin] + /api/skills/player/{playerId}: + get: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: List player skills + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/skills/player/{playerId}/{skillId}: + post: + tags: [Skills] + security: [ { bearerAuth: [] } ] + summary: Update player skill (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + level: { type: integer } + required: [level] + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + - in: path + name: skillId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + x-roles: [Admin] + /api/mission-categories: + get: + tags: [MissionCategories] + security: [ { bearerAuth: [] } ] + summary: List mission categories + responses: { '200': { description: OK } } + post: + tags: [MissionCategories] + security: [ { bearerAuth: [] } ] + summary: Create mission category (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + required: [title] + responses: { '201': { description: Created } } + x-roles: [Admin] + /api/mission-categories/{id}: + get: + tags: [MissionCategories] + security: [ { bearerAuth: [] } ] + summary: Get mission category + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + put: + tags: [MissionCategories] + security: [ { bearerAuth: [] } ] + summary: Update mission category (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + required: [title] + responses: { '200': { description: OK }, '404': { description: Not Found } } + x-roles: [Admin] + delete: + tags: [MissionCategories] + security: [ { bearerAuth: [] } ] + summary: Delete mission category (Admin) + responses: { '204': { description: No Content }, '404': { description: Not Found } } + x-roles: [Admin] + /api/missions/{id}: + get: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Get mission + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + put: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Update mission (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + description: { type: string } + missionCategoryId: { $ref: '#/components/schemas/Id' } + parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } + expReward: { type: integer } + manaReward: { type: integer } + required: [title,missionCategoryId,expReward,manaReward] + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + x-roles: [Admin] + delete: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Delete mission (Admin) + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '204': { description: No Content }, '404': { description: Not Found } } + x-roles: [Admin] + /api/missions/category/{categoryId}: + get: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Missions by category + parameters: + - in: path + name: categoryId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/missions/player/{playerId}/available: + get: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Available missions for player + parameters: + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/missions/{missionId}/rank-rules: + get: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Rank rules for mission + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/missions: + post: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Create mission (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + description: { type: string } + missionCategoryId: { $ref: '#/components/schemas/Id' } + parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } + expReward: { type: integer } + manaReward: { type: integer } + required: [title,missionCategoryId,expReward,manaReward] + responses: { '201': { description: Created } } + x-roles: [Admin] + /api/missions/{id}: + put: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Update mission (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + title: { type: string } + description: { type: string } + missionCategoryId: { $ref: '#/components/schemas/Id' } + parentMissionId: { $ref: '#/components/schemas/Id', nullable: true } + expReward: { type: integer } + manaReward: { type: integer } + required: [title,missionCategoryId,expReward,manaReward] + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + x-roles: [Admin] + delete: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Delete mission (Admin) + parameters: + - in: path + name: id + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '204': { description: No Content }, '404': { description: Not Found } } + x-roles: [Admin] + /api/missions/{missionId}/complete: + post: + tags: [Missions] + security: [ { bearerAuth: [] } ] + summary: Complete mission + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + requestBody: + content: + application/json: + schema: + type: object + properties: + playerId: { $ref: '#/components/schemas/Id' } + proof: { nullable: true } + required: [playerId] + responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/MissionCompletionResult' } } } }, '400': { description: Bad Request } } + /api/rewards/mission/{missionId}/skills: + get: + tags: [Rewards] + security: [ { bearerAuth: [] } ] + summary: Mission skill rewards + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/rewards/mission/{missionId}/items: + get: + tags: [Rewards] + security: [ { bearerAuth: [] } ] + summary: Mission item rewards + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/rewards/mission/{missionId}/can-claim/{playerId}: + get: + tags: [Rewards] + security: [ { bearerAuth: [] } ] + summary: Can claim mission rewards + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + - in: path + name: playerId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/rewards/mission/{missionId}/claim: + post: + tags: [Rewards] + security: [ { bearerAuth: [] } ] + summary: Claim mission rewards + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + requestBody: + content: + application/json: + schema: + type: object + properties: + playerId: { $ref: '#/components/schemas/Id' } + required: [playerId] + responses: { '200': { description: OK }, '409': { description: Conflict } } + /api/rewards/mission/{missionId}/force-distribute: + post: + tags: [Rewards] + security: [ { bearerAuth: [] } ] + summary: Force distribute mission rewards (Admin) + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + requestBody: + content: + application/json: + schema: + type: object + properties: + playerId: { $ref: '#/components/schemas/Id' } + required: [playerId] + responses: { '200': { description: OK } } + x-roles: [Admin] + /api/dialogue/mission/{missionId}: + get: + tags: [Dialogue] + security: [ { bearerAuth: [] } ] + summary: Get dialogue by mission + parameters: + - in: path + name: missionId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + /api/dialogue/message/{messageId}: + get: + tags: [Dialogue] + security: [ { bearerAuth: [] } ] + summary: Get dialogue message + parameters: + - in: path + name: messageId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK }, '404': { description: Not Found } } + /api/dialogue/message/{messageId}/options: + get: + tags: [Dialogue] + security: [ { bearerAuth: [] } ] + summary: Get dialogue response options + parameters: + - in: path + name: messageId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: { '200': { description: OK } } + /api/dialogue/message/{messageId}/respond: + post: + tags: [Dialogue] + security: [ { bearerAuth: [] } ] + summary: Respond to dialogue + parameters: + - in: path + name: messageId + required: true + schema: { $ref: '#/components/schemas/Id' } + requestBody: + content: + application/json: + schema: + type: object + properties: + responseOptionId: { $ref: '#/components/schemas/Id' } + playerId: { $ref: '#/components/schemas/Id' } + required: [responseOptionId,playerId] + responses: { '200': { description: OK } } + /api/dialogue: + post: + tags: [Dialogue] + security: [ { bearerAuth: [] } ] + summary: Create dialogue (Admin) + requestBody: + content: + application/json: + schema: + type: object + properties: + missionId: { $ref: '#/components/schemas/Id' } + initialDialogueMessageId: { $ref: '#/components/schemas/Id' } + interimDialogueMessageId: { $ref: '#/components/schemas/Id' } + endDialogueMessageId: { $ref: '#/components/schemas/Id' } + required: [missionId,initialDialogueMessageId,interimDialogueMessageId,endDialogueMessageId] + responses: { '201': { description: Created } } + x-roles: [Admin] + /api/profile/me: + get: + tags: [Profile] + security: [ { bearerAuth: [] } ] + summary: Get current user profile + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } + '404': { description: Not Found } + /api/profile/{userId}: + get: + tags: [Profile] + security: [ { bearerAuth: [] } ] + summary: Get profile by user id (Admin) + parameters: + - in: path + name: userId + required: true + schema: { $ref: '#/components/schemas/Id' } + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } + '404': { description: Not Found } + x-roles: [Admin] + /api/profile: + put: + tags: [Profile] + security: [ { bearerAuth: [] } ] + summary: Upsert current user profile + requestBody: + content: + application/json: + schema: + type: object + properties: + firstName: { type: string, nullable: true } + lastName: { type: string, nullable: true } + birthDate: { type: string, format: date, nullable: true } + about: { type: string, nullable: true } + location: { type: string, nullable: true } + responses: + '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } } + /api/profile/avatar: + post: + tags: [Profile] + security: [ { bearerAuth: [] } ] + summary: Upload avatar image (multipart/form-data) + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + '200': { description: OK, content: { application/json: { schema: { type: object, properties: { avatarUrl: { type: string } } } } } } + '400': { description: Bad Request } + delete: + tags: [Profile] + security: [ { bearerAuth: [] } ] + summary: Delete avatar image + responses: + '204': { description: No Content } + '404': { description: Not Found }