Add project files.
This commit is contained in:
parent
cfdca57c3c
commit
7c51dc217b
30
.dockerignore
Normal file
30
.dockerignore
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
!**/.gitignore
|
||||||
|
!.git/HEAD
|
||||||
|
!.git/config
|
||||||
|
!.git/packed-refs
|
||||||
|
!.git/refs/heads/**
|
||||||
21
.env.sample
Normal file
21
.env.sample
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Database Config
|
||||||
|
MARIADB_DATABASE=qtcnet
|
||||||
|
MARIADB_USER=qtcnet
|
||||||
|
MARIADB_PASSWORD=CHANGEME
|
||||||
|
MARIADB_RANDOM_ROOT_PASSWORD=true
|
||||||
|
|
||||||
|
# ASP.NET Config
|
||||||
|
ASPNETCORE_ENVIRONMENT="Production"
|
||||||
|
ASPNETCORE_HTTP_PORTS=8080
|
||||||
|
|
||||||
|
# Database Connection Config
|
||||||
|
DB_CONNECTION_STRING="Server=db;Database=qtcnet;User Id=qtcnet;Password=CHANGEME"
|
||||||
|
DB_PROVIDER="MySQL"
|
||||||
|
JWT_KEY="CHANGEME"
|
||||||
|
|
||||||
|
# Traefik Config (remove if not using)
|
||||||
|
TRAEFIK_HOST=qtc.alanmoon.net
|
||||||
|
TRAEFIK_CERT_PROVIDER=cloudflare
|
||||||
|
|
||||||
|
# Redis Connection Config
|
||||||
|
REDIS_CONNECTIONSTRING=redis
|
||||||
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Set default behavior to automatically normalize line endings.
|
||||||
|
###############################################################################
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set default behavior for command prompt diff.
|
||||||
|
#
|
||||||
|
# This is need for earlier builds of msysgit that does not have it on by
|
||||||
|
# default for csharp files.
|
||||||
|
# Note: This is only used by command line
|
||||||
|
###############################################################################
|
||||||
|
#*.cs diff=csharp
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Set the merge driver for project and solution files
|
||||||
|
#
|
||||||
|
# Merging from the command prompt will add diff markers to the files if there
|
||||||
|
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||||
|
# the diff markers are never inserted). Diff markers may cause the following
|
||||||
|
# file extensions to fail to load in VS. An alternative would be to treat
|
||||||
|
# these files as binary and thus will always conflict and require user
|
||||||
|
# intervention with every merge. To do so, just uncomment the entries below
|
||||||
|
###############################################################################
|
||||||
|
#*.sln merge=binary
|
||||||
|
#*.csproj merge=binary
|
||||||
|
#*.vbproj merge=binary
|
||||||
|
#*.vcxproj merge=binary
|
||||||
|
#*.vcproj merge=binary
|
||||||
|
#*.dbproj merge=binary
|
||||||
|
#*.fsproj merge=binary
|
||||||
|
#*.lsproj merge=binary
|
||||||
|
#*.wixproj merge=binary
|
||||||
|
#*.modelproj merge=binary
|
||||||
|
#*.sqlproj merge=binary
|
||||||
|
#*.wwaproj merge=binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# behavior for image files
|
||||||
|
#
|
||||||
|
# image files are treated as binary by default.
|
||||||
|
###############################################################################
|
||||||
|
#*.jpg binary
|
||||||
|
#*.png binary
|
||||||
|
#*.gif binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# diff behavior for common document formats
|
||||||
|
#
|
||||||
|
# Convert binary document formats to text before diffing them. This feature
|
||||||
|
# is only available from the command line. Turn it on by uncommenting the
|
||||||
|
# entries below.
|
||||||
|
###############################################################################
|
||||||
|
#*.doc diff=astextplain
|
||||||
|
#*.DOC diff=astextplain
|
||||||
|
#*.docx diff=astextplain
|
||||||
|
#*.DOCX diff=astextplain
|
||||||
|
#*.dot diff=astextplain
|
||||||
|
#*.DOT diff=astextplain
|
||||||
|
#*.pdf diff=astextplain
|
||||||
|
#*.PDF diff=astextplain
|
||||||
|
#*.rtf diff=astextplain
|
||||||
|
#*.RTF diff=astextplain
|
||||||
372
.gitignore
vendored
Normal file
372
.gitignore
vendored
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
/qtc-net-server/qtcdev.db
|
||||||
|
/qtc-net-server/qtcdev.db-shm
|
||||||
|
/qtc-net-server/qtcdev.db-wal
|
||||||
|
/qtc-net-server/run.Development.bat
|
||||||
|
/qtc-net-server/appsettings.json
|
||||||
|
/qtc-net-server/user-content/
|
||||||
|
/qtc-net-server/Properties/launchSettings.json
|
||||||
|
/qtc-net-server/appsettings.Development.json
|
||||||
|
/qtc-net-server/Resources/store.Development.json
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# QtC.NET Server
|
||||||
|
This is a from scratch SignalR based chat server that has the following features:
|
||||||
|
- Rooms
|
||||||
|
- Direct Messaging
|
||||||
|
- Precense
|
||||||
|
- Contacts
|
||||||
|
- Profiles
|
||||||
|
- Currency
|
||||||
|
- Stock Market
|
||||||
|
- Multiplayer Tic-Tac-Toe
|
||||||
|
- Email Verification
|
||||||
|
- Profile Cosmetics Store
|
||||||
|
|
||||||
|
# How To Build
|
||||||
|
In order to build, you need the .NET 9 SDK.
|
||||||
|
|
||||||
|
1. Clone This Repository
|
||||||
|
2. Run ``dotnet build`` Within The Folder That Contains The .csproj File
|
||||||
|
3. Configure And Enjoy!
|
||||||
|
|
||||||
|
# Docker Container (Recommended Self-Hosting Solution)
|
||||||
|
This Server Does Have A Docker Container You Can Create That Contains Your Config And User Content. Please Refer To `docker-compose.yml`
|
||||||
|
|
||||||
|
# JWT Keys
|
||||||
|
JWT Keys should be generated from scatch and added to your `docker-compose.yml` environment variables.
|
||||||
|
|
||||||
|
Anything Can Be Put Here, But For Security, It Should Be Within 32-Characters Long And Should Be Secure. An Example Key Is Provided, And Should Not Be Used In Production.
|
||||||
|
|
||||||
|
# .env File
|
||||||
|
The environment variable file is used to store sensitive info and settings, such as db passwords, keys, etc.
|
||||||
|
Most things are configured here, however some things are configured in `appsettings.json`
|
||||||
|
|
||||||
|
Please refer to `.env.sample` for more info.
|
||||||
16
docker-compose.dcproj
Normal file
16
docker-compose.dcproj
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectVersion>2.1</ProjectVersion>
|
||||||
|
<DockerTargetOS>Linux</DockerTargetOS>
|
||||||
|
<DockerPublishLocally>False</DockerPublishLocally>
|
||||||
|
<ProjectGuid>b1b3d2c9-92e6-4711-842e-c940a40a9a2e</ProjectGuid>
|
||||||
|
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||||
|
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/swagger</DockerServiceUrl>
|
||||||
|
<DockerServiceName>qtc-net-server</DockerServiceName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="docker-compose.yml" />
|
||||||
|
<None Include=".dockerignore" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
qtc-net-server:
|
||||||
|
container_name: qtc-server
|
||||||
|
env_file: .env
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: qtc-net-server/Dockerfile
|
||||||
|
volumes:
|
||||||
|
- "qtc-user-content:/app/user-content"
|
||||||
|
ports: # comment me out if your using traefik
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- qtc-backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
redis:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: false
|
||||||
|
# Traefik Config Example
|
||||||
|
# labels:
|
||||||
|
# - "traefik.enable=true"
|
||||||
|
# - "traefik.http.routers.qtcnet.rule=Host(`${TRAEFIK_HOST}`)"
|
||||||
|
# - "traefik.http.routers.qtcnet.tls=true"
|
||||||
|
# - "traefik.http.routers.qtcnet.tls.certresolver=${TRAEFIK_CERT_PROVIDER}"
|
||||||
|
# - "traefik.http.services.qtcnet.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: qtc-db
|
||||||
|
env_file: .env
|
||||||
|
image: mariadb
|
||||||
|
volumes:
|
||||||
|
- "qtc-data:/var/lib/mariadb"
|
||||||
|
networks:
|
||||||
|
- qtc-backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect"]
|
||||||
|
retries: 3
|
||||||
|
interval: 10s
|
||||||
|
timeout: 30s
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: qtc-cache
|
||||||
|
image: redis
|
||||||
|
networks:
|
||||||
|
- qtc-backend
|
||||||
|
ports:
|
||||||
|
- "6379:6379" # for dev access
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
qtc-data:
|
||||||
|
qtc-user-content:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
qtc-backend:
|
||||||
11
launchSettings.json
Normal file
11
launchSettings.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Docker Compose": {
|
||||||
|
"commandName": "DockerCompose",
|
||||||
|
"commandVersion": "1.0",
|
||||||
|
"serviceActions": {
|
||||||
|
"qtc-net-server": "StartDebugging"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
qtc-net-server.sln
Normal file
31
qtc-net-server.sln
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.33414.496
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "qtc-net-server", "qtc-net-server\qtc-net-server.csproj", "{AE9BEB1A-340C-4EE4-90D1-0B16456DDE6A}"
|
||||||
|
EndProject
|
||||||
|
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{B1B3D2C9-92E6-4711-842E-C940A40A9A2E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{AE9BEB1A-340C-4EE4-90D1-0B16456DDE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{AE9BEB1A-340C-4EE4-90D1-0B16456DDE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{AE9BEB1A-340C-4EE4-90D1-0B16456DDE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{AE9BEB1A-340C-4EE4-90D1-0B16456DDE6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B1B3D2C9-92E6-4711-842E-C940A40A9A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B1B3D2C9-92E6-4711-842E-C940A40A9A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B1B3D2C9-92E6-4711-842E-C940A40A9A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B1B3D2C9-92E6-4711-842E-C940A40A9A2E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {1B6C7F89-981F-4871-860E-D961CB6A204A}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
308
qtc-net-server/Controllers/AuthController.cs
Normal file
308
qtc-net-server/Controllers/AuthController.cs
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
using qtc_api.Services.EmailService;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/auth")]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
private readonly ITokenService _tokenService;
|
||||||
|
private readonly IHubContext<ChatHub> _chatGWContext;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
private readonly ServerConfig? serverConfig;
|
||||||
|
private readonly DataContext dataContext;
|
||||||
|
|
||||||
|
public AuthController(IUserService userService, ITokenService tokenService, IHubContext<ChatHub> chatGWContext, DataContext dataContext, IConfiguration configuration, IEmailService emailService)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_tokenService = tokenService;
|
||||||
|
_chatGWContext = chatGWContext;
|
||||||
|
_configuration = configuration;
|
||||||
|
_emailService = emailService;
|
||||||
|
|
||||||
|
serverConfig = JsonSerializer.Deserialize<ServerConfig>(JsonDocument.Parse(System.IO.File.ReadAllText("./Resources/ServerConfig.json")));
|
||||||
|
this.dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<User>>> Register(UserDto userDto)
|
||||||
|
{
|
||||||
|
if (userDto != null)
|
||||||
|
{
|
||||||
|
var response = await _userService.AddUser(userDto);
|
||||||
|
await _chatGWContext.Clients.All.SendAsync("RefreshUserLists");
|
||||||
|
if (response.Success != false && response.Data != null)
|
||||||
|
{
|
||||||
|
// send confirmation email (shouldn't do anything if email confirmation is disabled)
|
||||||
|
var confirmationToken = _tokenService.GenerateEmailConfirmationToken(response.Data);
|
||||||
|
var confirmationUrl = $"{Request.Scheme}://{Request.Host}/api/auth/verify-email?token={confirmationToken.Data}";
|
||||||
|
|
||||||
|
await _emailService.SendConfirmationEmail(response.Data.Email, response.Data.Username, confirmationUrl);
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return StatusCode(500, response.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<string>>> Login(UserLoginDto request)
|
||||||
|
{
|
||||||
|
var dbUser = await _userService.GetUserByEmail(request.Email);
|
||||||
|
|
||||||
|
if (dbUser.Data == null)
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<string>
|
||||||
|
{
|
||||||
|
Message = "User not found.",
|
||||||
|
Success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!BCrypt.Net.BCrypt.Verify(request.Password, dbUser.Data.PasswordHash))
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<string>
|
||||||
|
{
|
||||||
|
Message = "Incorrect password.",
|
||||||
|
Success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbUser.Data.IsEmailVerified && _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired"))
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<string>
|
||||||
|
{
|
||||||
|
Message = "You need to verify your email on this server. Check your inbox or spam. If you have not received an email, click 'Resend Verification Email'",
|
||||||
|
Success = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbUser.Data.Id == serverConfig?.AdminUserId && dbUser.Data.Role != "Admin")
|
||||||
|
{
|
||||||
|
dbUser.Data.Role = "Admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = await _tokenService.GenerateAccessTokenAndRefreshToken(dbUser.Data, true, request.RememberMe);
|
||||||
|
|
||||||
|
// check if the new user tag can be removed (14 days or 2 weeks)
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now.Date > dbUser.Data.CreatedAt.Date.AddDays(14) && dbUser.Data.Tags.Contains("new_user"))
|
||||||
|
{
|
||||||
|
List<string> _tagsList = [.. dbUser.Data.Tags];
|
||||||
|
_tagsList.Remove("new_user");
|
||||||
|
dbUser.Data.Tags = [.. _tagsList];
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataContext.SaveChangesAsync();
|
||||||
|
return Ok(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("refresh")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<string>>> RefreshLogin(string token)
|
||||||
|
{
|
||||||
|
var response = await _tokenService.ValidateRefreshToken(token);
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("resend-verification-email")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<bool>>> ResendVerificationEmail(string email)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByEmail(email);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var confirmationToken = _tokenService.GenerateEmailConfirmationToken(user.Data);
|
||||||
|
var confirmationUrl = $"{Request.Scheme}://{Request.Host}/api/auth/verify-email?token={confirmationToken.Data}";
|
||||||
|
|
||||||
|
await _emailService.SendConfirmationEmail(user.Data.Email, user.Data.Username, confirmationUrl);
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("request-password-reset")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<bool>>> SendPasswordResetEmail(string email)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByEmail(email);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var resetToken = _tokenService.GeneratePasswordResetConfirmationToken(user.Data); // we can probably use the same JWT structure for password resets
|
||||||
|
var resetUrl = $"{Request.Scheme}://{Request.Host}/api/auth/start-password-reset?token={resetToken.Data}";
|
||||||
|
|
||||||
|
await _emailService.SendPasswordResetEmail(user.Data.Email, user.Data.Username, resetUrl);
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reset-password")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<bool>>> ResetPassword(UserPasswordResetDto request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler()
|
||||||
|
{
|
||||||
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwtValidationResult = await handler.ValidateTokenAsync(request.Token, _tokenService.GetValidationParams().Data);
|
||||||
|
if (!jwtValidationResult.IsValid) return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||||
|
var jwtClaims = jwtValidationResult.ClaimsIdentity;
|
||||||
|
var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo;
|
||||||
|
|
||||||
|
if (!jwtClaims.HasClaim("TokenType", "client-password-reset")) return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||||
|
|
||||||
|
if (jwtClaims != null)
|
||||||
|
{
|
||||||
|
var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||||
|
var id = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (email != null && id != null)
|
||||||
|
{
|
||||||
|
// get the user from id claim
|
||||||
|
var user = await _userService.GetUserById(id.Value);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (user.Data.Email == email.Value && now < jwtValidTo.ToUniversalTime())
|
||||||
|
{
|
||||||
|
user.Data.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);
|
||||||
|
await dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "The User This Reset Token Is Associated With No Longer Exists." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||||
|
}
|
||||||
|
catch (SecurityTokenMalformedException)
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("verify-email")]
|
||||||
|
public async Task<ActionResult<string>> VerifyEmail(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler()
|
||||||
|
{
|
||||||
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwtValidationResult = await handler.ValidateTokenAsync(token, _tokenService.GetValidationParams().Data);
|
||||||
|
if (!jwtValidationResult.IsValid) return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
var jwtClaims = jwtValidationResult.ClaimsIdentity;
|
||||||
|
var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo;
|
||||||
|
|
||||||
|
if (!jwtClaims.HasClaim("TokenType", "email-confirmation")) return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
|
||||||
|
if (jwtClaims != null)
|
||||||
|
{
|
||||||
|
var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||||
|
var id = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (email != null && id != null)
|
||||||
|
{
|
||||||
|
// get the user from id claim
|
||||||
|
var user = await _userService.GetUserById(id.Value);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (user.Data.Email == email.Value && now < jwtValidTo.ToUniversalTime())
|
||||||
|
{
|
||||||
|
user.Data.IsEmailVerified = true;
|
||||||
|
await dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok("Email Verified! You may now login on the client.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok("The User This Confirmation Link Is Associated With No Longer Exists.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok("Token Invalid. You may need to be sent another confirmation link. You can do this by clicking 'Resend Verification Email' in the client.");
|
||||||
|
}
|
||||||
|
catch (SecurityTokenMalformedException)
|
||||||
|
{
|
||||||
|
return Ok("Token Invalid. You may need to be sent another confirmation link. You can do this by clicking 'Resend Verification Email' in the client.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("start-password-reset")]
|
||||||
|
public async Task<ActionResult<string>> StartPasswordReset(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler()
|
||||||
|
{
|
||||||
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwtValidationResult = await handler.ValidateTokenAsync(token, _tokenService.GetValidationParams().Data);
|
||||||
|
if (!jwtValidationResult.IsValid) return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
var jwtClaims = jwtValidationResult.ClaimsIdentity;
|
||||||
|
var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo;
|
||||||
|
|
||||||
|
if (!jwtClaims.HasClaim("TokenType", "password-reset")) return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
|
||||||
|
if (jwtClaims != null)
|
||||||
|
{
|
||||||
|
var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||||
|
var id = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (email != null && id != null)
|
||||||
|
{
|
||||||
|
// get the user from id claim
|
||||||
|
var user = await _userService.GetUserById(id.Value);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (user.Data.Email == email.Value && now < jwtValidTo.ToUniversalTime())
|
||||||
|
{
|
||||||
|
var clientToken = _tokenService.GeneratePasswordResetConfirmationToken(user.Data);
|
||||||
|
return Ok($"To Reset Your Password, Paste The Following Code Into Your Client And Enter A New Password\n{clientToken.Data}\n\nNOTE: This code is only valid for one hour.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Ok("The User This Reset Link Is Associated With No Longer Exists.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
}
|
||||||
|
catch (SecurityTokenMalformedException)
|
||||||
|
{
|
||||||
|
return Ok("Token Invalid. You may need to be sent another reset link.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
qtc-net-server/Controllers/ContactController.cs
Normal file
96
qtc-net-server/Controllers/ContactController.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using qtc_api.Services.ContactService;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/contacts")]
|
||||||
|
[ApiController]
|
||||||
|
public class ContactController : ControllerBase
|
||||||
|
{
|
||||||
|
private IContactService _contactService;
|
||||||
|
private IHubContext<ChatHub> _chatGwContext;
|
||||||
|
|
||||||
|
public ContactController(IContactService contactService, IHubContext<ChatHub> chatGwContext)
|
||||||
|
{
|
||||||
|
_contactService = contactService;
|
||||||
|
_chatGwContext = chatGwContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("add-contact")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<Contact>>> AddContact(string userId)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var ownerId = claims.First().Value;
|
||||||
|
|
||||||
|
var result = await _contactService.CreateContact(ownerId, userId);
|
||||||
|
|
||||||
|
// refresh contacts list for both users
|
||||||
|
await _chatGwContext.Clients.User(userId).SendAsync("RefreshContactsList");
|
||||||
|
await _chatGwContext.Clients.User(ownerId).SendAsync("RefreshContactsList");
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("approve-contact")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<bool>>> ApproveContact(string userId)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var ownerId = claims.First().Value;
|
||||||
|
|
||||||
|
var result = await _contactService.UpdateContactStatus(ownerId, userId, Contact.ContactStatus.Accepted, Contact.ContactStatus.Accepted);
|
||||||
|
|
||||||
|
// refresh contacts list for both users
|
||||||
|
await _chatGwContext.Clients.User(userId).SendAsync("RefreshContactsList");
|
||||||
|
await _chatGwContext.Clients.User(ownerId).SendAsync("RefreshContactsList");
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("remove-contact")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<Contact>>> RemoveContact(string userId)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var ownerId = claims.First().Value;
|
||||||
|
|
||||||
|
var response = await _contactService.DeleteContact(ownerId, userId);
|
||||||
|
|
||||||
|
// refresh contacts list for both users
|
||||||
|
await _chatGwContext.Clients.User(userId).SendAsync("RefreshContactsList");
|
||||||
|
await _chatGwContext.Clients.User(ownerId).SendAsync("RefreshContactsList");
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("get-user-contacts")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<List<Contact>>>> GetUserContacts(User user)
|
||||||
|
{
|
||||||
|
var result = await _contactService.GetUserContacts(user);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
qtc-net-server/Controllers/CurrencyGamesController.cs
Normal file
113
qtc-net-server/Controllers/CurrencyGamesController.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using qtc_api.Services.CurrencyGamesService;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using static qtc_api.Services.CurrencyGamesService.CurrencyGamesService;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/games")]
|
||||||
|
[ApiController]
|
||||||
|
public class CurrencyGamesController : ControllerBase
|
||||||
|
{
|
||||||
|
private IUserService _userService;
|
||||||
|
private CurrencyGamesService _currencyGamesService;
|
||||||
|
public CurrencyGamesController(IUserService userService, CurrencyGamesService currencyGamesService)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_currencyGamesService = currencyGamesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock Market
|
||||||
|
|
||||||
|
[HttpGet("stock-market/current-price")]
|
||||||
|
public ActionResult<ServiceResponse<int>> GetCurrentPricePerStock()
|
||||||
|
{
|
||||||
|
var price = _currencyGamesService.GetCurrentPricePerStock();
|
||||||
|
|
||||||
|
if (price != null && price.Success)
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<int>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = price.Data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<int>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Service Indicated Failiure"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("stock-market/buy-stock")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<UserStockActionResultDto>>> BuyStockFromMarket(int amount)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
|
||||||
|
var user = await _userService.GetUserById(id);
|
||||||
|
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var stocks = await _currencyGamesService.BuyStock(user.Data, amount);
|
||||||
|
|
||||||
|
if (stocks.Success)
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<UserStockActionResultDto> { Success = true, Data = stocks.Data });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = stocks.Message });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Identity User Not Found" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "No Identity" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("stock-market/sell-stock")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<UserStockActionResultDto>>> SellStockToMarket(int amount)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
|
||||||
|
var user = await _userService.GetUserById(id);
|
||||||
|
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var stocks = await _currencyGamesService.SellStock(user.Data, amount);
|
||||||
|
|
||||||
|
if (stocks.Success)
|
||||||
|
{
|
||||||
|
return Ok(new ServiceResponse<UserStockActionResultDto> { Success = true, Data = stocks.Data });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = stocks.Message });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Identity User Not Found" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "No Identity" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number Guess
|
||||||
|
|
||||||
|
[HttpGet("number-guess/get-number")]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<ServiceResponse<int>> GetRandomNumber()
|
||||||
|
{
|
||||||
|
return Ok(_currencyGamesService.GetRandomNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("number-guess/guess-number")]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<ServiceResponse<NumberGuessResult>> GuessNumber(int original, int guess)
|
||||||
|
{
|
||||||
|
return Ok(_currencyGamesService.GuessRandomNumber(original, guess));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
qtc-net-server/Controllers/GeneralController.cs
Normal file
17
qtc-net-server/Controllers/GeneralController.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/general")]
|
||||||
|
[ApiController]
|
||||||
|
public class GeneralController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<GeneralController> logger;
|
||||||
|
public GeneralController(ILogger<GeneralController> logger) => this.logger = logger;
|
||||||
|
|
||||||
|
[HttpGet("ping")]
|
||||||
|
public ActionResult PingAsync()
|
||||||
|
{
|
||||||
|
logger.LogInformation($"Ping Received From Client");
|
||||||
|
return Ok("Pong!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
qtc-net-server/Controllers/RoomsController.cs
Normal file
45
qtc-net-server/Controllers/RoomsController.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using qtc_api.Services.RoomService;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/rooms")]
|
||||||
|
[ApiController]
|
||||||
|
public class RoomsController : ControllerBase
|
||||||
|
{
|
||||||
|
public IRoomService _roomService;
|
||||||
|
public IHubContext<ChatHub> _hubContext;
|
||||||
|
|
||||||
|
public RoomsController(IRoomService roomService, IHubContext<ChatHub> hubContext)
|
||||||
|
{
|
||||||
|
_roomService = roomService;
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("create-room")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<Room>>> CreateRoom(string userId, RoomDto request)
|
||||||
|
{
|
||||||
|
var response = await _roomService.AddRoom(userId, request);
|
||||||
|
await _hubContext.Clients.All.SendAsync("RefreshRoomList");
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("delete-room")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<Room>>> DeleteRoom(string roomId)
|
||||||
|
{
|
||||||
|
var response = await _roomService.DeleteRoom(roomId);
|
||||||
|
await _hubContext.Clients.Group(roomId).SendAsync("RoomDeleted");
|
||||||
|
await _hubContext.Clients.All.SendAsync("RefreshRoomList");
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("get-all-rooms")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<List<Room>>>> GetAllRooms()
|
||||||
|
{
|
||||||
|
var rooms = await _roomService.GetAllRooms();
|
||||||
|
return Ok(rooms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
qtc-net-server/Controllers/StoreController.cs
Normal file
95
qtc-net-server/Controllers/StoreController.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using qtc_api.Services.StoreService;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/store")]
|
||||||
|
[ApiController]
|
||||||
|
public class StoreController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly StoreService _storeService;
|
||||||
|
public StoreController(StoreService storeService)
|
||||||
|
{
|
||||||
|
_storeService = storeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("all-items")]
|
||||||
|
public ActionResult<ServiceResponse<List<StoreItem>>> GetAllItems()
|
||||||
|
{
|
||||||
|
return Ok(_storeService.GetStoreItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("item")]
|
||||||
|
public ActionResult<ServiceResponse<StoreItem>> GetItem(int id)
|
||||||
|
{
|
||||||
|
return Ok(_storeService.GetStoreItem(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("bought-items")]
|
||||||
|
public ActionResult<ServiceResponse<List<OwnedStoreItem>>> GetBoughtStoreItems()
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var userId = claims.First().Value;
|
||||||
|
|
||||||
|
if (userId != null)
|
||||||
|
{
|
||||||
|
var result = _storeService.GetBoughtStoreItemsFromUser(userId);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "No UserId In Auth Header" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "No Auth Header" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("bought-item")]
|
||||||
|
public ActionResult<ServiceResponse<OwnedStoreItem>> GetBoughtItem(int id)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var userId = claims.First().Value;
|
||||||
|
|
||||||
|
if (userId != null)
|
||||||
|
{
|
||||||
|
var result = _storeService.GetBoughtStoreItemFromUser(userId, id);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "No UserId In Auth Header" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "No Auth Header" });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("buy-item")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<bool>>> BuyStoreItem(int id)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var userId = claims.First().Value;
|
||||||
|
|
||||||
|
if (userId != null)
|
||||||
|
{
|
||||||
|
var result = await _storeService.BuyStoreItem(userId, id);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<bool> { Success = false, Message = "No UserId In Auth Header" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<bool> { Success = false, Message = "No Auth Header" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
qtc-net-server/Controllers/TagController.cs
Normal file
29
qtc-net-server/Controllers/TagController.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using qtc_api.Services.BucketService;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/tags")]
|
||||||
|
public class TagController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IBucketService _bucketService;
|
||||||
|
public TagController(IBucketService bucketService)
|
||||||
|
{
|
||||||
|
_bucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{tagName}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> GetTagPicBytes(string tagName)
|
||||||
|
{
|
||||||
|
var _s3Res = await _bucketService.GetTagImageBytes(tagName);
|
||||||
|
if (_s3Res != null)
|
||||||
|
return new FileContentResult(_s3Res, "image/jpeg");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return NotFound(); // TODO - code the legacy way of finding images here (mostly for self-hosters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
217
qtc-net-server/Controllers/UsersController.cs
Normal file
217
qtc-net-server/Controllers/UsersController.cs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using qtc_api.Extensions;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace qtc_api.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/users")]
|
||||||
|
[ApiController]
|
||||||
|
public class UsersController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IDistributedCache _cache;
|
||||||
|
private readonly IHubContext<ChatHub> _chatGWContext;
|
||||||
|
public UsersController(IUserService userService, IDistributedCache distributedCache, IHubContext<ChatHub> chatGWContext)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_cache = distributedCache;
|
||||||
|
_chatGWContext = chatGWContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("all")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<List<UserInformationDto>>>> GetAllUsers()
|
||||||
|
{
|
||||||
|
var users = await _userService.GetAllUsers();
|
||||||
|
return Ok(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("user-info")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<UserInformationDto>>> GetUserInformation(string id)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserInformationById(id);
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("user-authorized")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<User>>> UserFromAuthorizeHead()
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
|
||||||
|
if (id != null)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserById(id);
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("Token did not contain an ID.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("Header not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("update")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<UserInformationDto>>> UpdateUserInformation(UserUpdateInformationDto user)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
|
||||||
|
if (id != null && id == user.Id)
|
||||||
|
{
|
||||||
|
var updatedUser = await _userService.UpdateUserInfo(user);
|
||||||
|
if (updatedUser.Success && updatedUser.Data != null)
|
||||||
|
{
|
||||||
|
await _chatGWContext.Clients.All.SendAsync("UpdateUser", user);
|
||||||
|
await _chatGWContext.Clients.User(updatedUser.Data.Id).SendAsync("UpdateCurrentUser");
|
||||||
|
|
||||||
|
return Ok(updatedUser);
|
||||||
|
}
|
||||||
|
else return Ok();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Unauthorized("You are not authorized to edit that user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("Session Expired.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("upload-profile-pic")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<string>>> UploadOrUpdateProfilePic(string userId, IFormFile file)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
|
||||||
|
if (id != null && id == userId)
|
||||||
|
{
|
||||||
|
if (file.Length > 3000000)
|
||||||
|
{
|
||||||
|
return BadRequest("File Is Above Limit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await _userService.UpdateUserPic(userId, file);
|
||||||
|
|
||||||
|
var res = await _userService.GetUserById(id);
|
||||||
|
User? user = res.Data;
|
||||||
|
|
||||||
|
UserInformationDto dto = new();
|
||||||
|
if (user != null)
|
||||||
|
dto.MapToUser(user);
|
||||||
|
|
||||||
|
await _chatGWContext.Clients.All.SendAsync("UpdateUser", dto);
|
||||||
|
|
||||||
|
// always try to overwrite cache when updating pfp
|
||||||
|
string recordId = $"UserPfp_{userId}";
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
await _cache.SetImageAsync(recordId, ms.ToArray(), TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
await _chatGWContext.Clients.User(id).SendAsync("UpdateCurrentUser");
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("You are not permitted to edit that user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("No Identity.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("profile-pic/{userId}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> GetUserProfilePicture(string userId)
|
||||||
|
{
|
||||||
|
string recordId = $"UserPfp_{userId}";
|
||||||
|
byte[]? pfpBytes = await _cache.GetImageAsync(recordId);
|
||||||
|
|
||||||
|
var result = new ServiceResponse<FileContentResult>();
|
||||||
|
if (pfpBytes == null)
|
||||||
|
{
|
||||||
|
result = await _userService.GetUserPic(userId);
|
||||||
|
if (result != null && result.Success && result.Data != null)
|
||||||
|
{
|
||||||
|
pfpBytes = result.Data.FileContents;
|
||||||
|
await _cache.SetImageAsync(recordId, pfpBytes, TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// explicitly set from cache
|
||||||
|
result.Success = true;
|
||||||
|
result.Data = new FileContentResult(pfpBytes, "image/jpeg");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null && result.Success != false)
|
||||||
|
{
|
||||||
|
return result.Data!;
|
||||||
|
}
|
||||||
|
else if (result!.Message == "User Does Not Have A Profile Picture." || result!.Message == "User Content Folder Does Not Exist Yet.")
|
||||||
|
{
|
||||||
|
return BadRequest("User has no profile picture.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return BadRequest("Failed To Get Profile Picture.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("delete-user")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public async Task<ActionResult<ServiceResponse<User>>> DeleteUserById(string id)
|
||||||
|
{
|
||||||
|
var result = await _userService.DeleteUser(id);
|
||||||
|
await _chatGWContext.Clients.All.SendAsync("RefreshUserLists");
|
||||||
|
if (result != null && result.Success && result.Data != null) await _chatGWContext.Clients.User(result.Data.Id).SendAsync("ForceSignOut");
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("update-user-currency")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ServiceResponse<int>>> UpdateUserCurrency(int amount, bool isSpinClaim)
|
||||||
|
{
|
||||||
|
var identity = HttpContext.User.Identity as ClaimsIdentity;
|
||||||
|
|
||||||
|
if (identity != null)
|
||||||
|
{
|
||||||
|
IEnumerable<Claim> claims = identity.Claims;
|
||||||
|
var id = claims.First().Value;
|
||||||
|
var _res = await _userService.AddCurrencyToUser(id, amount, isSpinClaim);
|
||||||
|
if (_res.Success)
|
||||||
|
{
|
||||||
|
await _chatGWContext.Clients.User(id).SendAsync("UpdateCurrentUser");
|
||||||
|
return Ok(_res);
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<int> { Success = false, Message = "Identity Has No User ID" });
|
||||||
|
}
|
||||||
|
else return Ok(new ServiceResponse<int> { Success = false, Message = "No Identity" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
qtc-net-server/Data/DataContext.cs
Normal file
75
qtc-net-server/Data/DataContext.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
namespace qtc_api.Data
|
||||||
|
{
|
||||||
|
public class DataContext : DbContext
|
||||||
|
{
|
||||||
|
public DataContext(DbContextOptions<DataContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<Room> Rooms { get; set; }
|
||||||
|
public DbSet<RefreshToken> ValidRefreshTokens { get; set; }
|
||||||
|
public DbSet<Contact> Contacts { get; set; }
|
||||||
|
public DbSet<OwnedStoreItem> OwnedStoreItems { get; set; }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
|
{
|
||||||
|
var dbProvider = Environment.GetEnvironmentVariable("DB_PROVIDER");
|
||||||
|
var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
|
||||||
|
|
||||||
|
if (dbProvider != null && connectionString != null)
|
||||||
|
{
|
||||||
|
switch (dbProvider)
|
||||||
|
{
|
||||||
|
case "MySQL":
|
||||||
|
options.UseMySQL(connectionString);
|
||||||
|
break;
|
||||||
|
case "SQLite":
|
||||||
|
options.UseSqlite(connectionString);
|
||||||
|
break;
|
||||||
|
default: throw new Exception("Unsupported Database Provider. Please Check 'DB_PROVIDER' Environment Variable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else throw new Exception("Cannot Find Environment Variables 'DB_PROVIDER' And 'DB_CONNECTION_STRING'. Please Check Environment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
// Users
|
||||||
|
|
||||||
|
builder.Entity<User>().HasMany(e => e.ContactsList);
|
||||||
|
builder.Entity<User>().HasMany(e => e.ContactsMade);
|
||||||
|
builder.Entity<User>().HasMany(e => e.OwnedStoreItems);
|
||||||
|
|
||||||
|
builder.Entity<User>()
|
||||||
|
.Property<string>("_tagString")
|
||||||
|
.HasColumnName("Tags");
|
||||||
|
|
||||||
|
// Rooms (no relations)
|
||||||
|
|
||||||
|
builder.Entity<Room>();
|
||||||
|
|
||||||
|
// Refresh Tokens
|
||||||
|
|
||||||
|
builder.Entity<RefreshToken>().HasOne(e => e.User)
|
||||||
|
.WithMany(e => e.RefreshTokens)
|
||||||
|
.HasForeignKey(e => e.UserID);
|
||||||
|
|
||||||
|
// Contacts
|
||||||
|
|
||||||
|
builder.Entity<Contact>().HasOne(e => e.Owner)
|
||||||
|
.WithMany(e => e.ContactsMade)
|
||||||
|
.HasForeignKey(e => e.OwnerId);
|
||||||
|
|
||||||
|
builder.Entity<Contact>().HasOne(e => e.User)
|
||||||
|
.WithMany(e => e.ContactsList)
|
||||||
|
.HasForeignKey(e => e.UserId);
|
||||||
|
|
||||||
|
// Purchased Store Items
|
||||||
|
|
||||||
|
builder.Entity<OwnedStoreItem>().HasOne(e => e.User)
|
||||||
|
.WithMany(e => e.OwnedStoreItems)
|
||||||
|
.HasForeignKey(e => e.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
qtc-net-server/Dockerfile
Normal file
21
qtc-net-server/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["qtc-net-server/qtc-net-server.csproj", "qtc-net-server/"]
|
||||||
|
RUN dotnet restore "qtc-net-server/qtc-net-server.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/qtc-net-server"
|
||||||
|
RUN dotnet build "qtc-net-server.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "qtc-net-server.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "qtc-net-server.dll"]
|
||||||
8
qtc-net-server/Dtos/Room/RoomDto.cs
Normal file
8
qtc-net-server/Dtos/Room/RoomDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Dtos.Room
|
||||||
|
{
|
||||||
|
public class RoomDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; } = new DateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Dtos/User/UserConnectionDto.cs
Normal file
8
qtc-net-server/Dtos/User/UserConnectionDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserConnectionDto
|
||||||
|
{
|
||||||
|
public Models.User? ConnectedUser { get; set; }
|
||||||
|
public string? ConnectionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
qtc-net-server/Dtos/User/UserDto.cs
Normal file
10
qtc-net-server/Dtos/User/UserDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public DateTime DateOfBirth { get; set; } = new DateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
qtc-net-server/Dtos/User/UserInformationDto.cs
Normal file
40
qtc-net-server/Dtos/User/UserInformationDto.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserInformationDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string ProfilePicture { get; set; } = string.Empty;
|
||||||
|
public string Bio { get; set; } = string.Empty;
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
public DateTime DateOfBirth { get; set; } = new DateTime();
|
||||||
|
public DateTime CreatedAt { get; set; } = new DateTime();
|
||||||
|
public DateTime LastLogin { get; set; } = new DateTime();
|
||||||
|
public int Status { get; set; } = 0;
|
||||||
|
public int CurrencyAmount { get; set; } = 0;
|
||||||
|
public int ProfileCosmeticId { get; set; } = 0;
|
||||||
|
public string TextStatus { get; set; } = string.Empty;
|
||||||
|
public string[] Tags { get; set; } = [];
|
||||||
|
|
||||||
|
public UserInformationDto MapToUser(Models.User user)
|
||||||
|
{
|
||||||
|
// map what is needed for info
|
||||||
|
Id = user.Id;
|
||||||
|
Username = user.Username;
|
||||||
|
ProfilePicture = user.ProfilePicture;
|
||||||
|
Bio = user.Bio;
|
||||||
|
Role = user.Role;
|
||||||
|
DateOfBirth = user.DateOfBirth;
|
||||||
|
CreatedAt = user.CreatedAt;
|
||||||
|
LastLogin = user.LastLogin;
|
||||||
|
Status = user.Status;
|
||||||
|
CurrencyAmount = user.CurrencyAmount;
|
||||||
|
ProfileCosmeticId = user.ActiveProfileCosmetic;
|
||||||
|
TextStatus = user.TextStatus;
|
||||||
|
Tags = user.Tags;
|
||||||
|
|
||||||
|
// return it
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
qtc-net-server/Dtos/User/UserLoginDto.cs
Normal file
9
qtc-net-server/Dtos/User/UserLoginDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserLoginDto
|
||||||
|
{
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public bool RememberMe { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Dtos/User/UserPasswordResetDto.cs
Normal file
8
qtc-net-server/Dtos/User/UserPasswordResetDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserPasswordResetDto
|
||||||
|
{
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Dtos/User/UserStatusDto.cs
Normal file
8
qtc-net-server/Dtos/User/UserStatusDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserStatusDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public int Status { get; set; } = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Dtos/User/UserStockActionResultDto.cs
Normal file
8
qtc-net-server/Dtos/User/UserStockActionResultDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserStockActionResultDto
|
||||||
|
{
|
||||||
|
public int StockAmount { get; set; }
|
||||||
|
public int CurrencyAmount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
qtc-net-server/Dtos/User/UserUpdateInformationDto.cs
Normal file
12
qtc-net-server/Dtos/User/UserUpdateInformationDto.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace qtc_api.Dtos.User
|
||||||
|
{
|
||||||
|
public class UserUpdateInformationDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Bio { get; set; } = string.Empty;
|
||||||
|
public DateTime DateOfBirth { get; set; } = new DateTime();
|
||||||
|
public int ProfileCosmeticId { get; set; } = 0;
|
||||||
|
public string TextStatus { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
qtc-net-server/Enums/GameStatus.cs
Normal file
13
qtc-net-server/Enums/GameStatus.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace qtc_api.Enums
|
||||||
|
{
|
||||||
|
public enum GameStatus
|
||||||
|
{
|
||||||
|
WaitingForPlayer,
|
||||||
|
SelectingSymbol,
|
||||||
|
Ongoing,
|
||||||
|
P1Win,
|
||||||
|
P2Win,
|
||||||
|
NoWin,
|
||||||
|
PlayerDisconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Enums/StoreItemType.cs
Normal file
8
qtc-net-server/Enums/StoreItemType.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Enums
|
||||||
|
{
|
||||||
|
public enum StoreItemType
|
||||||
|
{
|
||||||
|
ProfileCosmetic = 1,
|
||||||
|
ClientCosmetic = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
9
qtc-net-server/Enums/TicTacToeSymbol.cs
Normal file
9
qtc-net-server/Enums/TicTacToeSymbol.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace qtc_api.Enums
|
||||||
|
{
|
||||||
|
public enum TicTacToeSymbol
|
||||||
|
{
|
||||||
|
X,
|
||||||
|
O,
|
||||||
|
Blank
|
||||||
|
}
|
||||||
|
}
|
||||||
51
qtc-net-server/Extensions/DistributedCacheExtensions.cs
Normal file
51
qtc-net-server/Extensions/DistributedCacheExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace qtc_api.Extensions
|
||||||
|
{
|
||||||
|
public static class DistributedCacheExtensions
|
||||||
|
{
|
||||||
|
public static async Task SetRecordAsync<T>(this IDistributedCache cache, string recordId, T data, TimeSpan? absoluteExpireTime = null)
|
||||||
|
{
|
||||||
|
var options = new DistributedCacheEntryOptions();
|
||||||
|
|
||||||
|
options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
|
var jsonData = JsonSerializer.Serialize(data);
|
||||||
|
await cache.SetStringAsync(recordId, jsonData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SetImageAsync(this IDistributedCache cache, string recordId, byte[] data, TimeSpan? absoluteExpireTime = null)
|
||||||
|
{
|
||||||
|
var options = new DistributedCacheEntryOptions();
|
||||||
|
|
||||||
|
options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
|
await cache.SetAsync(recordId, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T?> GetRecordAsync<T>(this IDistributedCache cache, string recordId)
|
||||||
|
{
|
||||||
|
var jsonData = await cache.GetStringAsync(recordId);
|
||||||
|
|
||||||
|
if (jsonData == null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<byte[]?> GetImageAsync(this IDistributedCache cache, string recordId)
|
||||||
|
{
|
||||||
|
var bytes = await cache.GetAsync(recordId);
|
||||||
|
|
||||||
|
if (bytes == null)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
233
qtc-net-server/Hubs/ChatHub.cs
Normal file
233
qtc-net-server/Hubs/ChatHub.cs
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
using qtc_api.Services.RoomService;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace qtc_api.Hubs
|
||||||
|
{
|
||||||
|
public class ChatHub : Hub
|
||||||
|
{
|
||||||
|
private IUserService _userService;
|
||||||
|
private IRoomService _roomService;
|
||||||
|
private ILogger<ChatHub> _logger;
|
||||||
|
private DataContext _dataContext;
|
||||||
|
private ServerConfig _serverConfig;
|
||||||
|
|
||||||
|
public static readonly Dictionary<string, List<User>> GroupUsers = [];
|
||||||
|
public static readonly Dictionary<string, List<string>> ConnectedUsers = [];
|
||||||
|
public static readonly List<User> OnlineUsers = [];
|
||||||
|
|
||||||
|
private static User ServerUser = new()
|
||||||
|
{
|
||||||
|
Id = "0",
|
||||||
|
Username = "Server",
|
||||||
|
};
|
||||||
|
|
||||||
|
public ChatHub(IUserService userService, IRoomService roomService, ILogger<ChatHub> logger, DataContext dataContext, ServerConfig serverConfig)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_roomService = roomService;
|
||||||
|
_logger = logger;
|
||||||
|
_dataContext = dataContext;
|
||||||
|
_serverConfig = serverConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async override Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
Log("Client Connected To Hub");
|
||||||
|
|
||||||
|
var uid = Context.UserIdentifier;
|
||||||
|
if (uid != null)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserById(uid);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var userId = user.Data.Id;
|
||||||
|
|
||||||
|
lock (ConnectedUsers)
|
||||||
|
{
|
||||||
|
if (!ConnectedUsers.TryGetValue(userId, out List<string>? value))
|
||||||
|
{
|
||||||
|
value = [];
|
||||||
|
ConnectedUsers[userId] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Add(Context.ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OnlineUsers.Any(e => e.Id == userId))
|
||||||
|
{
|
||||||
|
Log("Setting User Online...");
|
||||||
|
OnlineUsers.Add(user.Data);
|
||||||
|
if (user.Data.Status == 0) await UpdateStatusAsync(user.Data, 1);
|
||||||
|
|
||||||
|
await Clients.Client(Context.ConnectionId).SendAsync("RefreshRoomList");
|
||||||
|
await Clients.Client(Context.ConnectionId).SendAsync("ReceiveServerConfig", _serverConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? ex)
|
||||||
|
{
|
||||||
|
Log("Client Disconnected From Hub");
|
||||||
|
|
||||||
|
var uid = Context.UserIdentifier;
|
||||||
|
if (uid != null)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserById(uid);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
var userId = user.Data.Id;
|
||||||
|
|
||||||
|
lock (ConnectedUsers)
|
||||||
|
{
|
||||||
|
if (ConnectedUsers.TryGetValue(userId, out List<string>? value))
|
||||||
|
{
|
||||||
|
value.Remove(Context.ConnectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConnectedUsers[userId].Count == 0)
|
||||||
|
{
|
||||||
|
ConnectedUsers.Remove(userId);
|
||||||
|
if (OnlineUsers.Any(e => e.Id == userId))
|
||||||
|
{
|
||||||
|
// set user offline if there aren't any more connections
|
||||||
|
var onlineUser = OnlineUsers.FirstOrDefault(e => e.Id == userId);
|
||||||
|
if (onlineUser != null)
|
||||||
|
{
|
||||||
|
Log("User Has No More Connections. Setting Offline...");
|
||||||
|
OnlineUsers.Remove(onlineUser);
|
||||||
|
if (user.Data.Status >= 1)
|
||||||
|
await UpdateStatusAsync(onlineUser, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// also remove the user from any rooms
|
||||||
|
var room = await _roomService.GetRoom(user.Data.CurrentRoomId);
|
||||||
|
if (room.Data != null)
|
||||||
|
await LeaveRoomAsync(user.Data, room.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("UpdateStatus")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task UpdateStatusAsync(User user, int status)
|
||||||
|
{
|
||||||
|
var statusDto = new UserStatusDto { Id = user.Id, Status = status };
|
||||||
|
|
||||||
|
Log($"Updating Status\n{JsonSerializer.Serialize(statusDto)}");
|
||||||
|
|
||||||
|
var res = await _userService.UpdateStatus(statusDto);
|
||||||
|
|
||||||
|
if (res != null && res.Success && res.Data != null)
|
||||||
|
{
|
||||||
|
// send UserUpdate to all clients
|
||||||
|
UserInformationDto userInfoDto = new();
|
||||||
|
userInfoDto.MapToUser(user);
|
||||||
|
|
||||||
|
await Clients.All.SendAsync("UserUpdate", userInfoDto);
|
||||||
|
|
||||||
|
Log($"Status Was Set To {res.Data.Status} On User {user.Username}");
|
||||||
|
await Clients.Client(Context.ConnectionId).SendAsync("UpdateCurrentUser");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Log($"Something Went Wrong Setting The Status On User {user.Username}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("JoinRoom")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task JoinRoomAsync(User user, Room room)
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, room.Id);
|
||||||
|
|
||||||
|
await Clients.Group(room.Id).SendAsync("RoomMessage", ServerUser, $"User {user.Username} Has Joined {room.Name}", room);
|
||||||
|
|
||||||
|
if (!GroupUsers.TryGetValue(room.Id, out _)) { GroupUsers.Add(room.Id, new List<User>()); }
|
||||||
|
GroupUsers[room.Id].Add(user);
|
||||||
|
|
||||||
|
await Clients.Group(room.Id).SendAsync("RoomUserList", room, GroupUsers[room.Id]);
|
||||||
|
Log($"User {user.Username} Has Joined {room.Name}");
|
||||||
|
|
||||||
|
user.CurrentRoomId = room.Id;
|
||||||
|
|
||||||
|
ServiceResponse<Room> dbRoomRes = await _roomService.GetRoom(room.Id);
|
||||||
|
Room? dbRoom = dbRoomRes?.Data;
|
||||||
|
|
||||||
|
if (dbRoom != null) dbRoom.UserCount += 1;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
await Clients.All.SendAsync("RefreshRoomList");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("JoinRoomGuest")]
|
||||||
|
public async Task JoinRoomGuestAsync(string roomId, string username)
|
||||||
|
{
|
||||||
|
// here we can just add the client to the room group and call it a day since the user isn't authenticated
|
||||||
|
var room = await _roomService.GetRoom(roomId);
|
||||||
|
|
||||||
|
if (room != null && room.Success && room.Data != null)
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, room.Data.Id);
|
||||||
|
|
||||||
|
await Clients.Group(room.Data.Id).SendAsync("RoomMessage", ServerUser, $"Guest User {username} Has Joined {room.Data.Name}", room);
|
||||||
|
|
||||||
|
room.Data.UserCount += 1;
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
await Clients.All.SendAsync("RefreshRoomList");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("LeaveRoom")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task LeaveRoomAsync(User user, Room room)
|
||||||
|
{
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id);
|
||||||
|
|
||||||
|
await Clients.Group(room.Id).SendAsync("RoomMessage", ServerUser, $"User {user.Username} Has Left {room.Name}", room);
|
||||||
|
|
||||||
|
if (GroupUsers.TryGetValue(room.Id, out _)) GroupUsers[room.Id].Remove(GroupUsers[room.Id].FirstOrDefault(e => e.Id == user.Id)!);
|
||||||
|
|
||||||
|
await Clients.Group(room.Id).SendAsync("RoomUserList", room, GroupUsers[room.Id]);
|
||||||
|
Log($"User {user.Username} Has Left {room.Name}");
|
||||||
|
|
||||||
|
user.CurrentRoomId = string.Empty;
|
||||||
|
|
||||||
|
ServiceResponse<Room> dbRoomRes = await _roomService.GetRoom(room.Id);
|
||||||
|
Room? dbRoom = dbRoomRes?.Data;
|
||||||
|
|
||||||
|
if (dbRoom != null) dbRoom.UserCount -= 1;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
await Clients.All.SendAsync("RefreshRoomList");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("SendMessage")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task SendMessageAsync(User user, Message message, Room room)
|
||||||
|
{
|
||||||
|
await Clients.Group(room.Id).SendAsync("RoomMessage", user, $"{message.Content}", room);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("SendDirectMessage")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task SendDirectMessageAsync(User user, UserInformationDto userToMsg, Message message)
|
||||||
|
{
|
||||||
|
// send direct message directly to connected user
|
||||||
|
if (ConnectedUsers.TryGetValue(userToMsg.Id, out List<string>? value))
|
||||||
|
{
|
||||||
|
var connection = value.FirstOrDefault();
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
UserInformationDto userInformationDto = new UserInformationDto { Id = user.Id, Username = user.Username, Bio = user.Bio, Role = user.Role, Status = user.Status, CreatedAt = user.CreatedAt, DateOfBirth = user.DateOfBirth, ProfilePicture = user.ProfilePicture, ProfileCosmeticId = user.ActiveProfileCosmetic };
|
||||||
|
await Clients.Client(connection).SendAsync("ReceiveDirectMessage", message, userInformationDto);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(string message) => _logger.LogInformation(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
272
qtc-net-server/Hubs/TicTacToeHub.cs
Normal file
272
qtc-net-server/Hubs/TicTacToeHub.cs
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
using qtc_api.Enums;
|
||||||
|
using qtc_api.Services.GameRoomService;
|
||||||
|
|
||||||
|
namespace qtc_api.Hubs
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
public class TicTacToeHub : Hub
|
||||||
|
{
|
||||||
|
private IUserService _userService;
|
||||||
|
private ILogger<TicTacToeHub> _logger;
|
||||||
|
private GameRoomService _gameRoomService;
|
||||||
|
public TicTacToeHub(ILogger<TicTacToeHub> logger, IUserService userService, GameRoomService gameRoomService)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
_gameRoomService = gameRoomService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
// find any existing room user was in
|
||||||
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => (e.Player1?.Id == Context.UserIdentifier) || (e.Player2?.Id == Context.UserIdentifier));
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
// just end the game
|
||||||
|
room.Status = GameStatus.PlayerDisconnected;
|
||||||
|
|
||||||
|
string usernameDisconnected = string.Empty;
|
||||||
|
|
||||||
|
// determine which player to null out
|
||||||
|
if (room.Player1?.Id == Context.UserIdentifier) { usernameDisconnected = room.Player1!.Username; room.Player1 = null; }
|
||||||
|
else { usernameDisconnected = room.Player2!.Username; room.Player2 = null; }
|
||||||
|
|
||||||
|
bool isWin = room.Status == GameStatus.P1Win || room.Status == GameStatus.P2Win || room.Status == GameStatus.NoWin;
|
||||||
|
if (!isWin)
|
||||||
|
await Clients.Group(room.Id).SendAsync("GameEnd", room, usernameDisconnected);
|
||||||
|
|
||||||
|
// remove the room
|
||||||
|
_gameRoomService.RemoveRoom(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("FindRoom")]
|
||||||
|
public async Task FindRoom()
|
||||||
|
{
|
||||||
|
// find user from useridentifier
|
||||||
|
var user = await _userService.GetUserById(Context.UserIdentifier!);
|
||||||
|
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
// find any room waiting on a player
|
||||||
|
var roomWOP = _gameRoomService.GameRooms.FirstOrDefault(e => e.Status == GameStatus.WaitingForPlayer && e.Player2 == null && e.Player1?.Id != Context.UserIdentifier);
|
||||||
|
if (roomWOP != null)
|
||||||
|
{
|
||||||
|
// add player to said group
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, roomWOP.Id);
|
||||||
|
|
||||||
|
// set player two
|
||||||
|
roomWOP.Player2 = user.Data;
|
||||||
|
|
||||||
|
// this game can now start symbol selection
|
||||||
|
roomWOP.Status = GameStatus.SelectingSymbol;
|
||||||
|
await Clients.User(roomWOP.Player1!.Id).SendAsync("SelectSymbol", roomWOP);
|
||||||
|
await Clients.Group(roomWOP.Id).SendAsync("SelectingSymbol", roomWOP);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find any room who's player one matches the user and is stuck in 'WaitingForPlayer' status
|
||||||
|
var roomWFPP1 = _gameRoomService.GameRooms.FirstOrDefault(e => e.Player1?.Id == user.Data.Id && e.Status == GameStatus.WaitingForPlayer);
|
||||||
|
if (roomWFPP1 != null)
|
||||||
|
// just remove it
|
||||||
|
_gameRoomService.RemoveRoom(roomWFPP1);
|
||||||
|
|
||||||
|
// if all if statements above don't match, make a new room
|
||||||
|
var gameRoom = new GameRoom { Id = Guid.NewGuid().ToString(), Player1 = user.Data, Status = GameStatus.WaitingForPlayer };
|
||||||
|
_gameRoomService.AddRoom(gameRoom);
|
||||||
|
|
||||||
|
// add user to group
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, gameRoom.Id);
|
||||||
|
|
||||||
|
// waiting for player
|
||||||
|
await Clients.Group(gameRoom.Id).SendAsync("WaitingForPlayer", gameRoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("SetSymbol")]
|
||||||
|
public async Task SetPlayerSymbol(string roomId, TicTacToeSymbol symbol)
|
||||||
|
{
|
||||||
|
// find the room
|
||||||
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
|
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
// set player one symbol
|
||||||
|
room.P1Symbol = symbol;
|
||||||
|
|
||||||
|
// set player two symbol based on what player one picked
|
||||||
|
switch (room.P1Symbol)
|
||||||
|
{
|
||||||
|
case TicTacToeSymbol.X:
|
||||||
|
room.P2Symbol = TicTacToeSymbol.O;
|
||||||
|
break;
|
||||||
|
case TicTacToeSymbol.O:
|
||||||
|
room.P2Symbol = TicTacToeSymbol.X;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the game can now start
|
||||||
|
room.Status = GameStatus.Ongoing;
|
||||||
|
await Clients.Group(room.Id).SendAsync("GameStart", room);
|
||||||
|
|
||||||
|
// start player ones turn
|
||||||
|
await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room);
|
||||||
|
await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("MakeMove")]
|
||||||
|
public async Task MakeMove(string roomId, TicTacToeMove move)
|
||||||
|
{
|
||||||
|
// find the room
|
||||||
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
|
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
// update board based on move (TOOD - figure out a better way to do this)
|
||||||
|
if (room.Player1?.Id == move.User.Id)
|
||||||
|
{
|
||||||
|
switch (move.Point)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
room.Board.Square1 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
room.Board.Square2 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
room.Board.Square3 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
room.Board.Square4 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
room.Board.Square5 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
room.Board.Square6 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
room.Board.Square7 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
room.Board.Square8 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
room.Board.Square9 = room.P1Symbol;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (move.Point)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
room.Board.Square1 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
room.Board.Square2 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
room.Board.Square3 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
room.Board.Square4 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
room.Board.Square5 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
room.Board.Square6 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
room.Board.Square7 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
room.Board.Square8 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
case 9:
|
||||||
|
room.Board.Square9 = room.P2Symbol;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send board update
|
||||||
|
await Clients.Group(roomId).SendAsync("UpdateBoard", room, move);
|
||||||
|
|
||||||
|
// check for draw
|
||||||
|
if (room.Board.IsDraw())
|
||||||
|
{
|
||||||
|
room.Status = GameStatus.NoWin;
|
||||||
|
await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for winner
|
||||||
|
var winningSymbol = room.Board.GetWinner();
|
||||||
|
if (winningSymbol != TicTacToeSymbol.Blank)
|
||||||
|
{
|
||||||
|
if (winningSymbol == room.P1Symbol)
|
||||||
|
{
|
||||||
|
// player one has won the game
|
||||||
|
room.Status = GameStatus.P1Win;
|
||||||
|
await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// player two has won the game
|
||||||
|
room.Status = GameStatus.P2Win;
|
||||||
|
await Clients.Group(room.Id).SendAsync("GameEnd", room, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch turns
|
||||||
|
if (move.User.Id == room.Player1?.Id)
|
||||||
|
{
|
||||||
|
await Clients.User(room.Player1!.Id).SendAsync("EndTurn", room);
|
||||||
|
await Clients.User(room.Player2!.Id).SendAsync("StartTurn", room);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room);
|
||||||
|
await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("RestartGame")]
|
||||||
|
public async Task RestartGame(string roomId)
|
||||||
|
{
|
||||||
|
// find the room
|
||||||
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
// room still exists and the game can be restarted
|
||||||
|
room.Status = GameStatus.Ongoing;
|
||||||
|
room.Board = new TicTacToeBoard(); // clears the board
|
||||||
|
|
||||||
|
await Clients.Group(room.Id).SendAsync("RestartGame", room);
|
||||||
|
|
||||||
|
// have player one start their turn
|
||||||
|
await Clients.User(room.Player1!.Id).SendAsync("StartTurn", room);
|
||||||
|
await Clients.User(room.Player2!.Id).SendAsync("EndTurn", room);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the user trying to restart the game needs to be informed that the other user has left
|
||||||
|
await Clients.Client(Context.ConnectionId).SendAsync("CannotRestart");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HubMethodName("SendRoomMessage")]
|
||||||
|
public async Task SendRoomMessage(string roomId, string message)
|
||||||
|
{
|
||||||
|
// find the room
|
||||||
|
var room = _gameRoomService.GameRooms.FirstOrDefault(e => e.Id == roomId);
|
||||||
|
if (room != null) await Clients.Group(room.Id).SendAsync("ReceiveMessage", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
qtc-net-server/Models/Contact.cs
Normal file
16
qtc-net-server/Models/Contact.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class Contact
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string OwnerId { get; set; } = string.Empty;
|
||||||
|
public string UserId { get; set; } = string.Empty;
|
||||||
|
public ContactStatus OwnerStatus { get; set; } = ContactStatus.AwaitingApprovalFromOther;
|
||||||
|
public ContactStatus UserStatus { get; set; } = ContactStatus.AwaitingApprovalFromSelf;
|
||||||
|
|
||||||
|
public virtual User? Owner { get; }
|
||||||
|
public virtual User? User { get; }
|
||||||
|
|
||||||
|
public enum ContactStatus { AwaitingApprovalFromOther = 0, AwaitingApprovalFromSelf = 1, Accepted = 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
qtc-net-server/Models/Message.cs
Normal file
10
qtc-net-server/Models/Message.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class Message
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string AuthorId { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; } = new DateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
qtc-net-server/Models/OwnedStoreItem.cs
Normal file
14
qtc-net-server/Models/OwnedStoreItem.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class OwnedStoreItem
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserId { get; set; } = string.Empty;
|
||||||
|
public int StoreItemId { get; set; }
|
||||||
|
|
||||||
|
public virtual User? User { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
qtc-net-server/Models/RefreshToken.cs
Normal file
12
qtc-net-server/Models/RefreshToken.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class RefreshToken
|
||||||
|
{
|
||||||
|
public string ID { get; set; } = string.Empty;
|
||||||
|
public string UserID { get; set; } = string.Empty;
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
|
||||||
|
public virtual User User { get; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
qtc-net-server/Models/Room.cs
Normal file
11
qtc-net-server/Models/Room.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class Room
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string CreatorId { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; } = new DateTime();
|
||||||
|
public int UserCount { get; set; } = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
qtc-net-server/Models/ServerConfig.cs
Normal file
11
qtc-net-server/Models/ServerConfig.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class ServerConfig
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? AdminUserId { get; set; }
|
||||||
|
public bool IsDown { get; set; }
|
||||||
|
public string? IsDownMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
qtc-net-server/Models/ServiceResponse.cs
Normal file
9
qtc-net-server/Models/ServiceResponse.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class ServiceResponse<T>
|
||||||
|
{
|
||||||
|
public T? Data { get; set; }
|
||||||
|
public bool Success { get; set; } = false;
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
qtc-net-server/Models/User.cs
Normal file
40
qtc-net-server/Models/User.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace qtc_api.Models
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string ProfilePicture { get; set; } = string.Empty;
|
||||||
|
public string Bio { get; set; } = string.Empty;
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
public string PasswordHash { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public bool IsEmailVerified { get; set; } = false;
|
||||||
|
public DateTime DateOfBirth { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public int Status { get; set; } = 0;
|
||||||
|
public int CurrencyAmount { get; set; } = 0;
|
||||||
|
public int StockAmount { get; set; } = 0;
|
||||||
|
public DateTime LastCurrencySpin { get; set; }
|
||||||
|
public int ActiveProfileCosmetic { get; set; } = 0;
|
||||||
|
public string CurrentRoomId { get; set; } = string.Empty;
|
||||||
|
public DateTime LastLogin { get; set; }
|
||||||
|
public string TextStatus { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private string? _tagString;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public string[] Tags
|
||||||
|
{
|
||||||
|
get => _tagString?.Split(',') ?? [];
|
||||||
|
set => _tagString = string.Join(',', value ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IEnumerable<RefreshToken>? RefreshTokens { get; }
|
||||||
|
public virtual IEnumerable<Contact>? ContactsMade { get; }
|
||||||
|
public virtual IEnumerable<Contact>? ContactsList { get; }
|
||||||
|
public virtual IEnumerable<OwnedStoreItem>? OwnedStoreItems { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
98
qtc-net-server/Program.cs
Normal file
98
qtc-net-server/Program.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
global using Microsoft.AspNetCore.Authorization;
|
||||||
|
global using Microsoft.AspNetCore.Mvc;
|
||||||
|
global using Microsoft.AspNetCore.SignalR;
|
||||||
|
global using Microsoft.EntityFrameworkCore;
|
||||||
|
global using Microsoft.IdentityModel.Tokens;
|
||||||
|
global using qtc_api.Data;
|
||||||
|
global using qtc_api.Dtos.Room;
|
||||||
|
global using qtc_api.Dtos.User;
|
||||||
|
global using qtc_api.Hubs;
|
||||||
|
global using qtc_api.Models;
|
||||||
|
global using qtc_api.Schema;
|
||||||
|
global using qtc_api.Services.TokenService;
|
||||||
|
global using qtc_api.Services.UserService;
|
||||||
|
global using System.Text;
|
||||||
|
using qtc_api.Services.BucketService;
|
||||||
|
using qtc_api.Services.ContactService;
|
||||||
|
using qtc_api.Services.CurrencyGamesService;
|
||||||
|
using qtc_api.Services.EmailService;
|
||||||
|
using qtc_api.Services.GameRoomService;
|
||||||
|
using qtc_api.Services.RoomService;
|
||||||
|
using qtc_api.Services.StoreService;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddDbContext<DataContext>();
|
||||||
|
builder.Services.AddSignalR().AddHubOptions<ChatHub>(options =>
|
||||||
|
{
|
||||||
|
options.EnableDetailedErrors = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddStackExchangeRedisCache(options =>
|
||||||
|
{
|
||||||
|
var redisConnectionString = Environment.GetEnvironmentVariable("REDIS_CONNECTIONSTRING");
|
||||||
|
if (redisConnectionString != null)
|
||||||
|
options.Configuration = redisConnectionString;
|
||||||
|
|
||||||
|
if (!builder.Environment.IsProduction())
|
||||||
|
options.InstanceName = "QtCNetServerDev_";
|
||||||
|
else
|
||||||
|
options.InstanceName = "QtCNetServer_";
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication().AddJwtBearer();
|
||||||
|
|
||||||
|
builder.Services.AddTransient<IEmailService, EmailService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IUserService, UserService>();
|
||||||
|
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||||
|
builder.Services.AddScoped<IRoomService, RoomService>();
|
||||||
|
builder.Services.AddScoped<IContactService, ContactService>();
|
||||||
|
builder.Services.AddScoped<StoreService>();
|
||||||
|
builder.Services.AddScoped<IBucketService, BucketService>();
|
||||||
|
builder.Services.AddScoped(provider => GetServerConfig());
|
||||||
|
|
||||||
|
ServerConfig GetServerConfig()
|
||||||
|
{
|
||||||
|
if (!File.Exists("./Resources/ServerConfig.json"))
|
||||||
|
throw new Exception("Server Config Doesn't Exist. Please Refer To The Repo For Examples.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var _jsonText = File.ReadAllText("./Resources/ServerConfig.json");
|
||||||
|
ServerConfig? _config = JsonSerializer.Deserialize<ServerConfig>(_jsonText);
|
||||||
|
if (_config != null)
|
||||||
|
return _config;
|
||||||
|
else throw new Exception("Invalid Server Config. Please Refer To The Repo For Examples.");
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid Server Config. Please Refer To The Repo For Examples.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<CurrencyGamesService>();
|
||||||
|
builder.Services.AddSingleton<GameRoomService>();
|
||||||
|
|
||||||
|
builder.Services.AddHostedService(provider => provider.GetService<CurrencyGamesService>()!);
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
using var scope = app.Services.CreateScope();
|
||||||
|
|
||||||
|
await scope.ServiceProvider.GetRequiredService<DataContext>().Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapHub<ChatHub>("/chat");
|
||||||
|
app.MapHub<TicTacToeHub>("/tttgame");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
7
qtc-net-server/Resources/ServerConfig.json
Normal file
7
qtc-net-server/Resources/ServerConfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"Name": "QtC.NET Server",
|
||||||
|
"Description": "This is a QtC.NET Server.",
|
||||||
|
"AdminUserId": "523736357658921388",
|
||||||
|
"IsDown": false,
|
||||||
|
"IsDownMessage": "This server is currently down. Please try again later."
|
||||||
|
}
|
||||||
11
qtc-net-server/Resources/store.json
Normal file
11
qtc-net-server/Resources/store.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Id": 1,
|
||||||
|
"Type": 1,
|
||||||
|
"Price": 100,
|
||||||
|
"Name": "Example",
|
||||||
|
"Description": "Change Me!",
|
||||||
|
"AssetUrl": "https://assets.alanmoon.net/qtc/cosmetics/test/test.gif",
|
||||||
|
"ThumbnailUrl": "https://assets.alanmoon.net/qtc/cosmetics/test/thumbnail.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
15
qtc-net-server/Schema/GameRoom.cs
Normal file
15
qtc-net-server/Schema/GameRoom.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using qtc_api.Enums;
|
||||||
|
|
||||||
|
namespace qtc_api.Schema
|
||||||
|
{
|
||||||
|
public class GameRoom
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public GameStatus Status { get; set; }
|
||||||
|
public TicTacToeBoard Board { get; set; } = new();
|
||||||
|
public User? Player1 { get; set; }
|
||||||
|
public TicTacToeSymbol P1Symbol { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public User? Player2 { get; set; }
|
||||||
|
public TicTacToeSymbol P2Symbol { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
qtc-net-server/Schema/StoreItem.cs
Normal file
23
qtc-net-server/Schema/StoreItem.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using qtc_api.Enums;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace qtc_api.Schema
|
||||||
|
{
|
||||||
|
public class StoreItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
[JsonPropertyName("Type")]
|
||||||
|
public StoreItemType Type { get; set; }
|
||||||
|
[JsonPropertyName("Price")]
|
||||||
|
public int Price { get; set; }
|
||||||
|
[JsonPropertyName("Name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("Description")]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("AssetUrl")]
|
||||||
|
public string AssetUrl { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("ThumbnailUrl")]
|
||||||
|
public string ThumbnailUrl { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
qtc-net-server/Schema/TicTacToeBoard.cs
Normal file
64
qtc-net-server/Schema/TicTacToeBoard.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using qtc_api.Enums;
|
||||||
|
|
||||||
|
namespace qtc_api.Schema
|
||||||
|
{
|
||||||
|
public class TicTacToeBoard
|
||||||
|
{
|
||||||
|
public TicTacToeSymbol Square1 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square2 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square3 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square4 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square5 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square6 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square7 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square8 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
public TicTacToeSymbol Square9 { get; set; } = TicTacToeSymbol.Blank;
|
||||||
|
|
||||||
|
public TicTacToeSymbol GetWinner()
|
||||||
|
{
|
||||||
|
// winning possibilities
|
||||||
|
var lines = new List<(TicTacToeSymbol, TicTacToeSymbol, TicTacToeSymbol)>
|
||||||
|
{
|
||||||
|
(Square1, Square2, Square3),
|
||||||
|
(Square4, Square5, Square6),
|
||||||
|
(Square7, Square8, Square9),
|
||||||
|
(Square1, Square4, Square7),
|
||||||
|
(Square2, Square5, Square8),
|
||||||
|
(Square3, Square6, Square9),
|
||||||
|
(Square1, Square5, Square9),
|
||||||
|
(Square3, Square5, Square7)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (a, b, c) in lines)
|
||||||
|
{
|
||||||
|
if (a != TicTacToeSymbol.Blank && a == b && b == c)
|
||||||
|
{
|
||||||
|
return a; // winner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TicTacToeSymbol.Blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDraw()
|
||||||
|
{
|
||||||
|
// If there is a winner, it's not a draw
|
||||||
|
if (GetWinner() != TicTacToeSymbol.Blank)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check if all squares are filled
|
||||||
|
bool allFilled =
|
||||||
|
Square1 != TicTacToeSymbol.Blank &&
|
||||||
|
Square2 != TicTacToeSymbol.Blank &&
|
||||||
|
Square3 != TicTacToeSymbol.Blank &&
|
||||||
|
Square4 != TicTacToeSymbol.Blank &&
|
||||||
|
Square5 != TicTacToeSymbol.Blank &&
|
||||||
|
Square6 != TicTacToeSymbol.Blank &&
|
||||||
|
Square7 != TicTacToeSymbol.Blank &&
|
||||||
|
Square8 != TicTacToeSymbol.Blank &&
|
||||||
|
Square9 != TicTacToeSymbol.Blank;
|
||||||
|
|
||||||
|
return allFilled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Schema/TicTacToeMove.cs
Normal file
8
qtc-net-server/Schema/TicTacToeMove.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Schema
|
||||||
|
{
|
||||||
|
public class TicTacToeMove
|
||||||
|
{
|
||||||
|
public User User { get; set; } = new();
|
||||||
|
public int Point { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
157
qtc-net-server/Services/BucketService/BucketService.cs
Normal file
157
qtc-net-server/Services/BucketService/BucketService.cs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
using Amazon.Runtime;
|
||||||
|
using Amazon.S3;
|
||||||
|
using Amazon.S3.Model;
|
||||||
|
|
||||||
|
namespace qtc_api.Services.BucketService
|
||||||
|
{
|
||||||
|
public class BucketService : IBucketService
|
||||||
|
{
|
||||||
|
public string ServiceUrl { get; private set; } = string.Empty;
|
||||||
|
public string AccessKey { get; private set; } = string.Empty;
|
||||||
|
public string SecretKey { get; private set; } = string.Empty;
|
||||||
|
public string ProfileImagesBucketName { get; private set; } = string.Empty;
|
||||||
|
public string CDNBucketName { get; private set; } = string.Empty;
|
||||||
|
public string ImagesBucketName { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
private static AmazonS3Client S3Client = new();
|
||||||
|
private IConfiguration _config;
|
||||||
|
private ILogger<BucketService> _logger;
|
||||||
|
|
||||||
|
private bool Enabled { get; set; }
|
||||||
|
public BucketService(IConfiguration config, ILogger<BucketService> logger)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
var serviceUrl = _config["S3Config:S3ServiceUrl"];
|
||||||
|
var accessKey = _config["S3Config:S3AccessKey"];
|
||||||
|
var secretKey = _config["S3Config:S3SecretKey"];
|
||||||
|
var profileImagesBucket = _config["S3Config:S3ProfileImagesBucket"];
|
||||||
|
var cdnBucket = _config["S3Config:S3CDNBucket"];
|
||||||
|
var imagesBucket = _config["S3Config:S3ImagesBucket"];
|
||||||
|
var enabled = _config.GetValue<bool>("S3Config:S3Enabled");
|
||||||
|
|
||||||
|
if (serviceUrl != null) ServiceUrl = serviceUrl;
|
||||||
|
if (accessKey != null) AccessKey = accessKey;
|
||||||
|
if (secretKey != null) SecretKey = secretKey;
|
||||||
|
if (profileImagesBucket != null) ProfileImagesBucketName = profileImagesBucket;
|
||||||
|
if (cdnBucket != null) CDNBucketName = cdnBucket;
|
||||||
|
if (imagesBucket != null) ImagesBucketName = imagesBucket;
|
||||||
|
|
||||||
|
Enabled = enabled;
|
||||||
|
|
||||||
|
var credentials = new BasicAWSCredentials(AccessKey, SecretKey);
|
||||||
|
S3Client = new AmazonS3Client(credentials, new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = ServiceUrl,
|
||||||
|
});
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PutProfileImage(string userId, string imageName, byte[] imageBytes)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Not Using S3 Bucket. Performance May Be Degraded.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(imageBytes);
|
||||||
|
var request = new PutObjectRequest()
|
||||||
|
{
|
||||||
|
Key = $@"{userId}/{imageName}",
|
||||||
|
BucketName = ProfileImagesBucketName,
|
||||||
|
InputStream = stream,
|
||||||
|
DisablePayloadSigning = true,
|
||||||
|
DisableDefaultChecksumValidation = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await S3Client.PutObjectAsync(request);
|
||||||
|
return response.HttpStatusCode == System.Net.HttpStatusCode.OK || response.HttpStatusCode == System.Net.HttpStatusCode.Accepted;
|
||||||
|
}
|
||||||
|
catch (AmazonS3Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"S3 Bucket Threw An Exception\n{ex.Message}\n{ex.StackTrace}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]?> GetProfileImageBytes(string userId, string imageName)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Not Using S3 Bucket. Performance May Be Degraded.");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await S3Client.GetObjectAsync(ProfileImagesBucketName, $@"{userId}/{imageName}");
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
using var stream = response.ResponseStream;
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
else return default;
|
||||||
|
}
|
||||||
|
catch (AmazonS3Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"S3 Bucket Threw An Exception\n{ex.Message}\n{ex.StackTrace}");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]?> GetTagImageBytes(string tagName)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Not Using S3 Bucket. Performance May Be Degraded.");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await S3Client.GetObjectAsync(CDNBucketName, $@"tags/{tagName}.png");
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
using var stream = response.ResponseStream;
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
else return default;
|
||||||
|
}
|
||||||
|
catch (AmazonS3Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"S3 Bucket Threw An Exception\n{ex.Message}\n{ex.StackTrace}");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteProfileImage(string userId, string imageName)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Not Using S3 Bucket. Performance May Be Degraded.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await S3Client.DeleteObjectAsync(ProfileImagesBucketName, $@"{userId}/{imageName}");
|
||||||
|
return response.HttpStatusCode == System.Net.HttpStatusCode.OK || response.HttpStatusCode == System.Net.HttpStatusCode.Accepted;
|
||||||
|
}
|
||||||
|
catch (AmazonS3Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"S3 Bucket Threw An Exception\n{ex.Message}\n{ex.StackTrace}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
qtc-net-server/Services/BucketService/IBucketService.cs
Normal file
15
qtc-net-server/Services/BucketService/IBucketService.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
namespace qtc_api.Services.BucketService
|
||||||
|
{
|
||||||
|
public interface IBucketService
|
||||||
|
{
|
||||||
|
public string ServiceUrl { get; }
|
||||||
|
public string AccessKey { get; }
|
||||||
|
public string SecretKey { get; }
|
||||||
|
public string ProfileImagesBucketName { get; }
|
||||||
|
public string ImagesBucketName { get; }
|
||||||
|
public Task<bool> PutProfileImage(string userId, string imageName, byte[] imageBytes);
|
||||||
|
public Task<byte[]?> GetProfileImageBytes(string userId, string imageName);
|
||||||
|
public Task<byte[]?> GetTagImageBytes(string tagName);
|
||||||
|
public Task<bool> DeleteProfileImage(string userId, string imageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
qtc-net-server/Services/ContactService/ContactService.cs
Normal file
119
qtc-net-server/Services/ContactService/ContactService.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
namespace qtc_api.Services.ContactService
|
||||||
|
{
|
||||||
|
public class ContactService : IContactService
|
||||||
|
{
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public ContactService(DataContext dataContext)
|
||||||
|
{
|
||||||
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Contact>> CreateContact(string ownerId, string userId)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Contact>();
|
||||||
|
var rnd = LongRandom(1, 900000000000000000, new Random());
|
||||||
|
|
||||||
|
var contact = new Contact()
|
||||||
|
{
|
||||||
|
Id = rnd.ToString(),
|
||||||
|
OwnerId = ownerId,
|
||||||
|
UserId = userId,
|
||||||
|
OwnerStatus = Contact.ContactStatus.AwaitingApprovalFromOther,
|
||||||
|
UserStatus = Contact.ContactStatus.AwaitingApprovalFromSelf,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dataContext.Contacts.AddAsync(contact);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = contact;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<bool>> UpdateContactStatus(string ownerId, string userId, Contact.ContactStatus ownerStatus, Contact.ContactStatus userStatus)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<bool>();
|
||||||
|
|
||||||
|
var contact = await _dataContext.Contacts.FirstOrDefaultAsync(e => (e.OwnerId == ownerId && e.UserId == userId) || (e.OwnerId == userId && e.UserId == ownerId));
|
||||||
|
|
||||||
|
if (contact != null)
|
||||||
|
{
|
||||||
|
contact.OwnerStatus = ownerStatus;
|
||||||
|
contact.UserStatus = userStatus;
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
}
|
||||||
|
else serviceResponse.Success = false;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Contact>> DeleteContact(string ownerId, string userId)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Contact>();
|
||||||
|
var contact = await _dataContext.Contacts.FirstOrDefaultAsync(e => (e.OwnerId == ownerId && e.UserId == userId) || (e.OwnerId == userId && e.UserId == ownerId));
|
||||||
|
|
||||||
|
if (contact != null)
|
||||||
|
{
|
||||||
|
var result = _dataContext.Contacts.Remove(contact);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = result.Entity;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Data = null;
|
||||||
|
serviceResponse.Message = "Contact Not Found";
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Contact>> GetContactById(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Contact>();
|
||||||
|
|
||||||
|
var contact = await _dataContext.Contacts.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (contact == null) { serviceResponse.Success = false; serviceResponse.Message = "Contact Does Not Exist."; }
|
||||||
|
else { serviceResponse.Success = true; serviceResponse.Data = contact; }
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<List<Contact>>> GetUserContacts(User user)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<List<Contact>>();
|
||||||
|
|
||||||
|
var contactsMade = await _dataContext.Contacts.Where(x => x.OwnerId == user.Id).ToListAsync();
|
||||||
|
var contactsList = await _dataContext.Contacts.Where(x => x.UserId == user.Id).ToListAsync();
|
||||||
|
|
||||||
|
var contactsCombined = new List<Contact>();
|
||||||
|
|
||||||
|
foreach (var contact in contactsMade)
|
||||||
|
contactsCombined.Add(contact); // all contacts the user has made
|
||||||
|
foreach (var contact in contactsList)
|
||||||
|
contactsCombined.Add(contact); // all contacts the user has received
|
||||||
|
|
||||||
|
if (contactsCombined.Count == 0) { serviceResponse.Success = true; serviceResponse.Message = "User Has No Contacts."; }
|
||||||
|
else { serviceResponse.Success = true; serviceResponse.Data = contactsCombined; }
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long LongRandom(long min, long max, Random rnd)
|
||||||
|
{
|
||||||
|
long result = rnd.Next((int)(min >> 32), (int)(max >> 32));
|
||||||
|
result = result << 32;
|
||||||
|
result = result | (long)rnd.Next((int)min, (int)max);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
qtc-net-server/Services/ContactService/IContactService.cs
Normal file
11
qtc-net-server/Services/ContactService/IContactService.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace qtc_api.Services.ContactService
|
||||||
|
{
|
||||||
|
public interface IContactService
|
||||||
|
{
|
||||||
|
public Task<ServiceResponse<Contact>> CreateContact(string ownerId, string userId);
|
||||||
|
public Task<ServiceResponse<bool>> UpdateContactStatus(string ownerId, string userId, Contact.ContactStatus ownerStatus, Contact.ContactStatus userStatus);
|
||||||
|
public Task<ServiceResponse<List<Contact>>> GetUserContacts(User user);
|
||||||
|
public Task<ServiceResponse<Contact>> GetContactById(string id);
|
||||||
|
public Task<ServiceResponse<Contact>> DeleteContact(string ownerId, string userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
namespace qtc_api.Services.CurrencyGamesService
|
||||||
|
{
|
||||||
|
public class CurrencyGamesService : IHostedService, IDisposable
|
||||||
|
{
|
||||||
|
private Timer? Timer { get; set; }
|
||||||
|
private int CurrentPricePerStock { get; set; }
|
||||||
|
|
||||||
|
private IServiceScopeFactory _scopeFactory;
|
||||||
|
private ILogger<CurrencyGamesService> logger;
|
||||||
|
public CurrencyGamesService(IServiceScopeFactory scopeFactory, ILogger<CurrencyGamesService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock Market
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Timer = new Timer(Timer_Elapsed, null, TimeSpan.Zero, TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
|
logger.LogInformation("Stock Market Loop Started");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Timer?.Change(Timeout.Infinite, 0);
|
||||||
|
|
||||||
|
logger.LogInformation("Stock Market Loop Stopped");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Timer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<int> GetCurrentPricePerStock() { return new ServiceResponse<int> { Success = true, Data = CurrentPricePerStock }; }
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<UserStockActionResultDto>> BuyStock(User user, int amount)
|
||||||
|
{
|
||||||
|
using (var scope = _scopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var userServiceScoped = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < amount; i++)
|
||||||
|
{
|
||||||
|
total += CurrentPricePerStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total > user.CurrencyAmount) return new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Cannot Afford Stock(s)" };
|
||||||
|
|
||||||
|
var currencyResult = await userServiceScoped.RemoveCurrencyFromUser(user.Id, total);
|
||||||
|
var stockResult = await userServiceScoped.AddStockToUser(user.Id, amount);
|
||||||
|
|
||||||
|
return new ServiceResponse<UserStockActionResultDto> { Success = true, Data = new UserStockActionResultDto { StockAmount = stockResult.Data, CurrencyAmount = currencyResult.Data } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<UserStockActionResultDto>> SellStock(User user, int amount)
|
||||||
|
{
|
||||||
|
using (var scope = _scopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var userServiceScoped = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||||
|
|
||||||
|
if (amount > user.StockAmount) return new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Not Enough Stocks" };
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < amount; i++)
|
||||||
|
{
|
||||||
|
total += CurrentPricePerStock;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currencyResult = await userServiceScoped.AddCurrencyToUser(user.Id, total, false);
|
||||||
|
var stockResult = await userServiceScoped.RemoveStockFromUser(user.Id, amount);
|
||||||
|
|
||||||
|
return new ServiceResponse<UserStockActionResultDto> { Success = true, Data = new UserStockActionResultDto { StockAmount = stockResult.Data, CurrencyAmount = currencyResult.Data } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number Guess
|
||||||
|
|
||||||
|
public ServiceResponse<int> GetRandomNumber()
|
||||||
|
{
|
||||||
|
Random rnd = new Random();
|
||||||
|
return new ServiceResponse<int> { Success = true, Data = rnd.Next(1, 100) };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<NumberGuessResult> GuessRandomNumber(int original, int guess)
|
||||||
|
{
|
||||||
|
var response = new ServiceResponse<NumberGuessResult> { Success = true };
|
||||||
|
|
||||||
|
if (original - guess == 10) response.Data = NumberGuessResult.Lower;
|
||||||
|
else if (guess - original == 10) response.Data = NumberGuessResult.Higher;
|
||||||
|
else if (guess == original) response.Data = NumberGuessResult.Correct;
|
||||||
|
else response.Data = NumberGuessResult.Incorrect;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Timer_Elapsed(object? state)
|
||||||
|
{
|
||||||
|
Random rnd = new Random();
|
||||||
|
CurrentPricePerStock = rnd.Next(5, 200);
|
||||||
|
logger.LogInformation($"Current Price Per Stock - {CurrentPricePerStock}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NumberGuessResult
|
||||||
|
{
|
||||||
|
Higher,
|
||||||
|
Lower,
|
||||||
|
Correct,
|
||||||
|
Incorrect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
qtc-net-server/Services/EmailService/EmailService.cs
Normal file
135
qtc-net-server/Services/EmailService/EmailService.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Mail;
|
||||||
|
|
||||||
|
namespace qtc_api.Services.EmailService
|
||||||
|
{
|
||||||
|
public class EmailService : IEmailService
|
||||||
|
{
|
||||||
|
private IConfiguration _configuration;
|
||||||
|
private ILogger<EmailService> _logger;
|
||||||
|
public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendConfirmationEmail(string email, string name, string confirmUrl)
|
||||||
|
{
|
||||||
|
// get config entries
|
||||||
|
bool confirmationRequired = _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired");
|
||||||
|
string? host = _configuration.GetValue<string>("EmailConfig:SMTPServer");
|
||||||
|
int? port = _configuration.GetValue<int?>("EmailConfig:SMTPPort");
|
||||||
|
string? username = _configuration.GetValue<string>("EmailConfig:SMTPUsername");
|
||||||
|
string? password = _configuration.GetValue<string>("EmailConfig:SMTPPassword");
|
||||||
|
string? senderAddress = _configuration.GetValue<string>("EmailConfig:SMTPSenderAddress");
|
||||||
|
|
||||||
|
if (!confirmationRequired)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Email Confirmation Is Disabled. For Better Security Of Your Instance, Consider Setting Up An Email Sender Service.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (host == null || username == null || password == null || senderAddress == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Email Confirmation Is Enabled But No SMTP Settings Were Set.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set email subject
|
||||||
|
string emailSubject = "QtC.NET Email Confirmation";
|
||||||
|
|
||||||
|
// build confirmation email body
|
||||||
|
StringBuilder emailBody = new();
|
||||||
|
emailBody.AppendLine($"<h2>Hello {name},</h2>");
|
||||||
|
emailBody.AppendLine("<p>Your receiving this message because you made a QtC.NET Account on a server that requires email confirmation.<br>");
|
||||||
|
emailBody.AppendLine(@$"You can confirm your account by clicking <a href=""{confirmUrl}"">here.</a><br>");
|
||||||
|
emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.<br><br>");
|
||||||
|
emailBody.AppendLine("If you did not create a QtC.NET account on any server, you may simply ignore this email.</p>");
|
||||||
|
|
||||||
|
// create new client
|
||||||
|
using var client = new SmtpClient(host, port ?? 587)
|
||||||
|
{
|
||||||
|
Credentials = new NetworkCredential(username, password),
|
||||||
|
EnableSsl = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// send email
|
||||||
|
MailMessage mailMessage = new();
|
||||||
|
|
||||||
|
MailAddress.TryCreate(senderAddress, out MailAddress? from);
|
||||||
|
if (from == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MailAddress.TryCreate(email, out MailAddress? to);
|
||||||
|
if (to == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mailMessage.IsBodyHtml = true;
|
||||||
|
mailMessage.To.Add(to);
|
||||||
|
mailMessage.From = from;
|
||||||
|
mailMessage.Subject = emailSubject;
|
||||||
|
mailMessage.Body = emailBody.ToString();
|
||||||
|
|
||||||
|
await client.SendMailAsync(mailMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendPasswordResetEmail(string email, string name, string passwordResetUrl)
|
||||||
|
{
|
||||||
|
// get config entries
|
||||||
|
bool confirmationRequired = _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired");
|
||||||
|
string? host = _configuration.GetValue<string>("EmailConfig:SMTPServer");
|
||||||
|
int? port = _configuration.GetValue<int?>("EmailConfig:SMTPPort");
|
||||||
|
string? username = _configuration.GetValue<string>("EmailConfig:SMTPUsername");
|
||||||
|
string? password = _configuration.GetValue<string>("EmailConfig:SMTPPassword");
|
||||||
|
string? senderAddress = _configuration.GetValue<string>("EmailConfig:SMTPSenderAddress");
|
||||||
|
|
||||||
|
if (!confirmationRequired)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Email Confirmation Is Disabled. For Better Security Of Your Instance, Consider Setting Up An Email Sender Service.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (host == null || username == null || password == null || senderAddress == null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Email Confirmation Is Enabled But No SMTP Settings Were Set.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set email subject
|
||||||
|
string emailSubject = "QtC.NET Email Confirmation";
|
||||||
|
|
||||||
|
// build confirmation email body
|
||||||
|
StringBuilder emailBody = new();
|
||||||
|
emailBody.AppendLine($"<h2>Hello {name},</h2>");
|
||||||
|
emailBody.AppendLine("<p>Your receiving this message because you requested a password reset on a QtC.NET Account.<br>");
|
||||||
|
emailBody.AppendLine(@$"You can reset your password by clicking <a href=""{passwordResetUrl}"">here.</a><br>");
|
||||||
|
emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.<br><br>");
|
||||||
|
emailBody.AppendLine("If you did not request a password reset on any QtC.NET account on any server, you may simply ignore this email.</p>");
|
||||||
|
|
||||||
|
// create new client
|
||||||
|
using var client = new SmtpClient(host, port ?? 587)
|
||||||
|
{
|
||||||
|
Credentials = new NetworkCredential(username, password),
|
||||||
|
EnableSsl = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// send email
|
||||||
|
MailMessage mailMessage = new();
|
||||||
|
|
||||||
|
MailAddress.TryCreate(senderAddress, out MailAddress? from);
|
||||||
|
if (from == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MailAddress.TryCreate(email, out MailAddress? to);
|
||||||
|
if (to == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mailMessage.IsBodyHtml = true;
|
||||||
|
mailMessage.To.Add(to);
|
||||||
|
mailMessage.From = from;
|
||||||
|
mailMessage.Subject = emailSubject;
|
||||||
|
mailMessage.Body = emailBody.ToString();
|
||||||
|
|
||||||
|
await client.SendMailAsync(mailMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
qtc-net-server/Services/EmailService/IEmailService.cs
Normal file
8
qtc-net-server/Services/EmailService/IEmailService.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace qtc_api.Services.EmailService
|
||||||
|
{
|
||||||
|
public interface IEmailService
|
||||||
|
{
|
||||||
|
public Task SendConfirmationEmail(string email, string name, string confirmUrl);
|
||||||
|
public Task SendPasswordResetEmail(string email, string name, string passwordResetUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
qtc-net-server/Services/GameRoomService/GameRoomService.cs
Normal file
10
qtc-net-server/Services/GameRoomService/GameRoomService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace qtc_api.Services.GameRoomService
|
||||||
|
{
|
||||||
|
public class GameRoomService
|
||||||
|
{
|
||||||
|
public List<GameRoom> GameRooms { get; set; } = [];
|
||||||
|
|
||||||
|
public void AddRoom(GameRoom room) => GameRooms.Add(room);
|
||||||
|
public void RemoveRoom(GameRoom room) => GameRooms.Remove(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
qtc-net-server/Services/RoomService/IRoomService.cs
Normal file
11
qtc-net-server/Services/RoomService/IRoomService.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace qtc_api.Services.RoomService
|
||||||
|
{
|
||||||
|
public interface IRoomService
|
||||||
|
{
|
||||||
|
public Task<ServiceResponse<Room>> GetRoom(string id);
|
||||||
|
public Task<ServiceResponse<List<Room>>> GetAllRooms();
|
||||||
|
public Task<ServiceResponse<Room>> AddRoom(string userId, RoomDto room);
|
||||||
|
public Task<ServiceResponse<Room>> UpdateRoom(Room room);
|
||||||
|
public Task<ServiceResponse<Room>> DeleteRoom(string id);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
qtc-net-server/Services/RoomService/RoomService.cs
Normal file
110
qtc-net-server/Services/RoomService/RoomService.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
namespace qtc_api.Services.RoomService
|
||||||
|
{
|
||||||
|
public class RoomService : IRoomService
|
||||||
|
{
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
private long idMax = 900000000000000000;
|
||||||
|
|
||||||
|
public RoomService(DataContext dataContext)
|
||||||
|
{
|
||||||
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Room>> AddRoom(string userId, RoomDto room)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Room>();
|
||||||
|
var roomList = await _dataContext.Rooms.ToListAsync();
|
||||||
|
|
||||||
|
var roomToAdd = new Room();
|
||||||
|
Random rnd = new Random();
|
||||||
|
|
||||||
|
roomToAdd.Id = LongRandom(1, idMax, rnd).ToString();
|
||||||
|
roomToAdd.Name = room.Name;
|
||||||
|
roomToAdd.CreatorId = userId;
|
||||||
|
roomToAdd.CreatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var cRoom = await _dataContext.Rooms.FirstOrDefaultAsync(x => x.Name == roomToAdd.Name);
|
||||||
|
|
||||||
|
if (cRoom == null)
|
||||||
|
{
|
||||||
|
await _dataContext.Rooms.AddAsync(roomToAdd);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = roomToAdd;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Room already exists.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Room>> DeleteRoom(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Room>();
|
||||||
|
var room = await _dataContext.Rooms.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
_dataContext.Rooms.Remove(room);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = room;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Room not found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<List<Room>>> GetAllRooms()
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<List<Room>>();
|
||||||
|
var rooms = await _dataContext.Rooms.ToListAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = rooms;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<Room>> GetRoom(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<Room>();
|
||||||
|
var room = await _dataContext.Rooms.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = room;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Room not found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ServiceResponse<Room>> UpdateRoom(Room room)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long LongRandom(long min, long max, Random rnd)
|
||||||
|
{
|
||||||
|
long result = rnd.Next((int)(min >> 32), (int)(max >> 32));
|
||||||
|
result = result << 32;
|
||||||
|
result = result | (long)rnd.Next((int)min, (int)max);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
qtc-net-server/Services/StoreService/StoreService.cs
Normal file
95
qtc-net-server/Services/StoreService/StoreService.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace qtc_api.Services.StoreService
|
||||||
|
{
|
||||||
|
public class StoreService
|
||||||
|
{
|
||||||
|
public static List<StoreItem> StoreItems { get; set; } = [];
|
||||||
|
|
||||||
|
private readonly DataContext _ctx;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
public StoreService(DataContext ctx, IUserService userService)
|
||||||
|
{
|
||||||
|
_ctx = ctx;
|
||||||
|
_userService = userService;
|
||||||
|
|
||||||
|
string storeJsonLocation = "./Resources/store.json";
|
||||||
|
|
||||||
|
if (System.Diagnostics.Debugger.IsAttached)
|
||||||
|
storeJsonLocation = "./Resources/store.Development.json";
|
||||||
|
|
||||||
|
StoreItem[]? storeItems = JsonSerializer.Deserialize<StoreItem[]>(File.ReadAllText(storeJsonLocation));
|
||||||
|
if (storeItems != null && storeItems.Length > 0)
|
||||||
|
{
|
||||||
|
StoreItems = storeItems.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<List<StoreItem>> GetStoreItems()
|
||||||
|
{
|
||||||
|
return new ServiceResponse<List<StoreItem>> { Success = true, Data = StoreItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<StoreItem> GetStoreItem(int id)
|
||||||
|
{
|
||||||
|
var item = StoreItems.FirstOrDefault(e => e.Id == id);
|
||||||
|
if (item != null)
|
||||||
|
return new ServiceResponse<StoreItem> { Success = true, Data = item };
|
||||||
|
else return new ServiceResponse<StoreItem> { Success = false, Message = "Item Not Found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<OwnedStoreItem> GetBoughtStoreItemFromUser(string userId, int itemId)
|
||||||
|
{
|
||||||
|
// find item owned by user
|
||||||
|
var item = _ctx.OwnedStoreItems.FirstOrDefault(e => e.UserId == userId && e.StoreItemId == itemId);
|
||||||
|
if (item != null)
|
||||||
|
return new ServiceResponse<OwnedStoreItem> { Success = true, Data = item };
|
||||||
|
else return new ServiceResponse<OwnedStoreItem> { Success = false, Message = "Item Not Yet Purchased" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<List<OwnedStoreItem>> GetBoughtStoreItemsFromUser(string userId)
|
||||||
|
{
|
||||||
|
// find items owned by user
|
||||||
|
var items = _ctx.OwnedStoreItems.Where(e => e.UserId == userId).ToList();
|
||||||
|
if (items != null && items.Count > 0)
|
||||||
|
return new ServiceResponse<List<OwnedStoreItem>> { Success = true, Data = items };
|
||||||
|
else return new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "User Owns No Items" };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<bool>> BuyStoreItem(string userId, int id)
|
||||||
|
{
|
||||||
|
// find item in store
|
||||||
|
var item = StoreItems.FirstOrDefault(e => e.Id == id);
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
// deduct currency from user
|
||||||
|
var user = await _userService.GetUserById(userId);
|
||||||
|
if (user != null && user.Success && user.Data != null)
|
||||||
|
{
|
||||||
|
if (user.Data.CurrencyAmount >= item.Price)
|
||||||
|
{
|
||||||
|
// remove currency from user
|
||||||
|
await _userService.RemoveCurrencyFromUser(userId, item.Price);
|
||||||
|
|
||||||
|
// create owned item
|
||||||
|
OwnedStoreItem ownedStoreItem = new OwnedStoreItem
|
||||||
|
{
|
||||||
|
StoreItemId = item.Id,
|
||||||
|
UserId = userId
|
||||||
|
};
|
||||||
|
|
||||||
|
// add to table
|
||||||
|
_ctx.OwnedStoreItems.Add(ownedStoreItem);
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
// return successful service response
|
||||||
|
return new ServiceResponse<bool> { Success = true, Data = true };
|
||||||
|
}
|
||||||
|
else return new ServiceResponse<bool> { Success = false, Message = "Insufficient Currency" };
|
||||||
|
}
|
||||||
|
else return new ServiceResponse<bool> { Success = false, Message = "User Not Found" };
|
||||||
|
}
|
||||||
|
else return new ServiceResponse<bool> { Success = false, Message = "Item Not Found" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
qtc-net-server/Services/TokenService/ITokenService.cs
Normal file
12
qtc-net-server/Services/TokenService/ITokenService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace qtc_api.Services.TokenService
|
||||||
|
{
|
||||||
|
public interface ITokenService
|
||||||
|
{
|
||||||
|
public Task<ServiceResponse<string>> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false);
|
||||||
|
public ServiceResponse<string> GenerateEmailConfirmationToken(User user);
|
||||||
|
public ServiceResponse<string> GeneratePasswordResetConfirmationToken(User user);
|
||||||
|
public Task<ServiceResponse<bool>> ValidateAccessToken(string accessToken);
|
||||||
|
public Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken);
|
||||||
|
public ServiceResponse<TokenValidationParameters> GetValidationParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
285
qtc-net-server/Services/TokenService/TokenService.cs
Normal file
285
qtc-net-server/Services/TokenService/TokenService.cs
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace qtc_api.Services.TokenService
|
||||||
|
{
|
||||||
|
public class TokenService : ITokenService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
public TokenService(IConfiguration configuration, DataContext dataContext)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_dataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<string>> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken, bool remember)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
// Generate JWT Access Token
|
||||||
|
|
||||||
|
List<Claim> claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Name, user.Username),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim(ClaimTypes.Role, user.Role),
|
||||||
|
new Claim("TokenType", "access")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||||
|
var issuer = _configuration["Jwt:Issuer"];
|
||||||
|
var audience = _configuration["Jwt:Audience"];
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: issuer,
|
||||||
|
audience: audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(1),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
serviceResponse.Data = jwt;
|
||||||
|
|
||||||
|
// Generate and Store Refresh Token
|
||||||
|
|
||||||
|
if (generateRefToken)
|
||||||
|
{
|
||||||
|
var random = new byte[32];
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(random);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshToken refToken = new RefreshToken()
|
||||||
|
{
|
||||||
|
ID = LongRandom(1, 900000000000000000, new Random()).ToString(),
|
||||||
|
UserID = user.Id,
|
||||||
|
Token = Convert.ToBase64String(random)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (remember) refToken.Expires = DateTime.UtcNow.AddMonths(1);
|
||||||
|
else refToken.Expires = DateTime.UtcNow.AddDays(3);
|
||||||
|
|
||||||
|
_dataContext.ValidRefreshTokens.Add(refToken);
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Message = refToken.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<string> GenerateEmailConfirmationToken(User user)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
// Generate JWT Access Token
|
||||||
|
|
||||||
|
List<Claim> claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim("TokenType", "email-confirmation")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||||
|
var issuer = _configuration["Jwt:Issuer"];
|
||||||
|
var audience = _configuration["Jwt:Audience"];
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: issuer,
|
||||||
|
audience: audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(24),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = jwt;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<string> GeneratePasswordResetConfirmationToken(User user)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
// Generate JWT Access Token
|
||||||
|
|
||||||
|
List<Claim> claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim("TokenType", "password-reset")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||||
|
var issuer = _configuration["Jwt:Issuer"];
|
||||||
|
var audience = _configuration["Jwt:Audience"];
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: issuer,
|
||||||
|
audience: audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(1),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = jwt;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<string> GenerateClientPasswordResetConfirmationToken(User user)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
// Generate JWT Access Token
|
||||||
|
|
||||||
|
List<Claim> claims = new List<Claim>()
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||||
|
new Claim(ClaimTypes.Email, user.Email),
|
||||||
|
new Claim("TokenType", "client-password-reset")
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||||
|
var issuer = _configuration["Jwt:Issuer"];
|
||||||
|
var audience = _configuration["Jwt:Audience"];
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: issuer,
|
||||||
|
audience: audience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(1),
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
|
||||||
|
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = jwt;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
var dbRefresh = await _dataContext.ValidRefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken);
|
||||||
|
|
||||||
|
if (dbRefresh != null)
|
||||||
|
{
|
||||||
|
if (DateTime.Compare(DateTime.UtcNow, dbRefresh.Expires) > 0)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Refresh Token Expired.";
|
||||||
|
|
||||||
|
// Handle Expired Refresh Token
|
||||||
|
|
||||||
|
_dataContext.ValidRefreshTokens.Remove(dbRefresh);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == dbRefresh.UserID);
|
||||||
|
|
||||||
|
if (user != null && dbRefresh.UserID == user.Id)
|
||||||
|
{
|
||||||
|
var token = await GenerateAccessTokenAndRefreshToken(user, true, false);
|
||||||
|
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Message = token.Message;
|
||||||
|
serviceResponse.Data = token.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Requesting User ID and the associated Refresh Token's User ID does not match.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Invalid Refresh Token.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<bool>> ValidateAccessToken(string accessToken)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<bool>();
|
||||||
|
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var validationParams = GetValidationParams();
|
||||||
|
|
||||||
|
TokenValidationResult result = await tokenHandler.ValidateTokenAsync(accessToken, validationParams.Data);
|
||||||
|
|
||||||
|
if (result.IsValid)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = true;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = false;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResponse<TokenValidationParameters> GetValidationParams()
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<TokenValidationParameters>();
|
||||||
|
|
||||||
|
serviceResponse.Data = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = _configuration["Jwt:Issuer"],
|
||||||
|
ValidAudience = _configuration["Jwt:Audience"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!))
|
||||||
|
};
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long LongRandom(long min, long max, Random rnd)
|
||||||
|
{
|
||||||
|
long result = rnd.Next((int)(min >> 32), (int)(max >> 32));
|
||||||
|
result = result << 32;
|
||||||
|
result = result | (long)rnd.Next((int)min, (int)max);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
qtc-net-server/Services/UserService/IUserService.cs
Normal file
21
qtc-net-server/Services/UserService/IUserService.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace qtc_api.Services.UserService
|
||||||
|
{
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
public Task<ServiceResponse<User>> GetUserById(string id);
|
||||||
|
public Task<ServiceResponse<UserInformationDto>> GetUserInformationById(string id);
|
||||||
|
public Task<ServiceResponse<User>> GetUserByEmail(string email);
|
||||||
|
public Task<ServiceResponse<List<UserInformationDto>>> GetAllUsers();
|
||||||
|
public Task<ServiceResponse<List<UserInformationDto>>> GetAllOnlineUsers();
|
||||||
|
public Task<ServiceResponse<User>> AddUser(UserDto userReq);
|
||||||
|
public Task<ServiceResponse<UserInformationDto>> UpdateUserInfo(UserUpdateInformationDto request);
|
||||||
|
public Task<ServiceResponse<string>> UpdateUserPic(string userId, IFormFile file);
|
||||||
|
public Task<ServiceResponse<FileContentResult>> GetUserPic(string userId);
|
||||||
|
public Task<ServiceResponse<UserStatusDto>> UpdateStatus(UserStatusDto request);
|
||||||
|
public Task<ServiceResponse<User>> DeleteUser(string id);
|
||||||
|
public Task<ServiceResponse<int>> AddCurrencyToUser(string id, int amount, bool isSpinClaim);
|
||||||
|
public Task<ServiceResponse<int>> RemoveCurrencyFromUser(string id, int amount);
|
||||||
|
public Task<ServiceResponse<int>> AddStockToUser(string id, int amount);
|
||||||
|
public Task<ServiceResponse<int>> RemoveStockFromUser(string id, int amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
507
qtc-net-server/Services/UserService/UserService.cs
Normal file
507
qtc-net-server/Services/UserService/UserService.cs
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
using qtc_api.Services.BucketService;
|
||||||
|
|
||||||
|
namespace qtc_api.Services.UserService
|
||||||
|
{
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IBucketService _bucketService;
|
||||||
|
private readonly DataContext _dataContext;
|
||||||
|
|
||||||
|
private long idMax = 900000000000000000;
|
||||||
|
|
||||||
|
public UserService(IConfiguration configuration, IBucketService bucketService, DataContext dataContext)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_dataContext = dataContext;
|
||||||
|
_bucketService = bucketService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<User>> AddUser(UserDto userReq)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<User>();
|
||||||
|
var userList = await _dataContext.Users.ToListAsync();
|
||||||
|
|
||||||
|
var user = new User();
|
||||||
|
Random rnd = new Random();
|
||||||
|
var id = LongRandom(1, idMax, rnd);
|
||||||
|
|
||||||
|
user.Id = id.ToString();
|
||||||
|
user.Username = userReq.Username;
|
||||||
|
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(userReq.Password);
|
||||||
|
user.Email = userReq.Email;
|
||||||
|
user.DateOfBirth = userReq.DateOfBirth;
|
||||||
|
user.Role = _configuration["Jwt:DefaultUserRole"]!;
|
||||||
|
user.CreatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// add new user tag
|
||||||
|
user.Tags = ["new_user"];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cUser = userList.FirstOrDefault(x => x.Email == user.Email);
|
||||||
|
|
||||||
|
if (cUser != null)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User with that email already exists.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _dataContext.Users.AddAsync(user);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
userList = await _dataContext.Users.ToListAsync();
|
||||||
|
|
||||||
|
if (userList.Contains(user))
|
||||||
|
{
|
||||||
|
var createdUser = await _dataContext.Users.FindAsync(user.Id);
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = createdUser;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User not created.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = ex.Message;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<User>> DeleteUser(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<User>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(x => x.Id == id)!;
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
_dataContext.Users.Remove(user);
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = user;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User not found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<List<UserInformationDto>>> GetAllUsers()
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<List<UserInformationDto>>();
|
||||||
|
var userList = await _dataContext.Users.ToListAsync();
|
||||||
|
|
||||||
|
var userInfoList = new List<UserInformationDto>();
|
||||||
|
foreach (var user in userList)
|
||||||
|
{
|
||||||
|
var x = new UserInformationDto();
|
||||||
|
|
||||||
|
x.Id = user.Id;
|
||||||
|
x.Username = user.Username;
|
||||||
|
x.ProfilePicture = user.ProfilePicture;
|
||||||
|
x.Role = user.Role;
|
||||||
|
x.Bio = user.Bio;
|
||||||
|
x.DateOfBirth = user.DateOfBirth;
|
||||||
|
x.CurrencyAmount = user.CurrencyAmount;
|
||||||
|
x.Status = user.Status;
|
||||||
|
x.CreatedAt = user.CreatedAt;
|
||||||
|
x.ProfileCosmeticId = user.ActiveProfileCosmetic;
|
||||||
|
x.LastLogin = user.LastLogin;
|
||||||
|
|
||||||
|
userInfoList.Add(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = userInfoList;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<List<UserInformationDto>>> GetAllOnlineUsers()
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<List<UserInformationDto>>();
|
||||||
|
var onlineUsers = new List<UserInformationDto>();
|
||||||
|
|
||||||
|
await _dataContext.Users.ForEachAsync(user =>
|
||||||
|
{
|
||||||
|
if (user.Status == 1 || user.Status == 2 || user.Status == 3)
|
||||||
|
{
|
||||||
|
var x = new UserInformationDto();
|
||||||
|
|
||||||
|
x.Id = user.Id;
|
||||||
|
x.Username = user.Username;
|
||||||
|
x.ProfilePicture = user.ProfilePicture;
|
||||||
|
x.Role = user.Role;
|
||||||
|
x.Bio = user.Bio;
|
||||||
|
x.Status = user.Status;
|
||||||
|
x.DateOfBirth = user.DateOfBirth;
|
||||||
|
x.CreatedAt = user.CreatedAt;
|
||||||
|
x.CurrencyAmount = user.CurrencyAmount;
|
||||||
|
x.CreatedAt = user.CreatedAt;
|
||||||
|
x.ProfileCosmeticId = user.ActiveProfileCosmetic;
|
||||||
|
x.LastLogin = user.LastLogin;
|
||||||
|
|
||||||
|
onlineUsers.Add(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = onlineUsers;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<User>> GetUserById(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<User>();
|
||||||
|
var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = user;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<UserInformationDto>> GetUserInformationById(string id)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<UserInformationDto>();
|
||||||
|
var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var dto = new UserInformationDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.Username,
|
||||||
|
ProfilePicture = user.ProfilePicture,
|
||||||
|
Role = user.Role,
|
||||||
|
Bio = user.Bio,
|
||||||
|
DateOfBirth = user.DateOfBirth,
|
||||||
|
CreatedAt = user.CreatedAt,
|
||||||
|
Status = user.Status,
|
||||||
|
CurrencyAmount = user.CurrencyAmount,
|
||||||
|
ProfileCosmeticId = user.ActiveProfileCosmetic,
|
||||||
|
LastLogin = user.LastLogin,
|
||||||
|
TextStatus = user.TextStatus,
|
||||||
|
Tags = user.Tags
|
||||||
|
};
|
||||||
|
|
||||||
|
// always add admin tag if user is admin
|
||||||
|
if (user.Role == "Admin")
|
||||||
|
{
|
||||||
|
List<string> _tagList = [.. dto.Tags];
|
||||||
|
_tagList.Add("admin");
|
||||||
|
dto.Tags = [.. _tagList];
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = dto;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User not found.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<User>> GetUserByEmail(string email)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<User>();
|
||||||
|
var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Email == email);
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = user;
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<UserInformationDto>> UpdateUserInfo(UserUpdateInformationDto request)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<UserInformationDto>();
|
||||||
|
var dbUser = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == request.Id);
|
||||||
|
|
||||||
|
if (dbUser != null)
|
||||||
|
{
|
||||||
|
dbUser.Username = request.Username;
|
||||||
|
dbUser.Bio = request.Bio;
|
||||||
|
dbUser.DateOfBirth = request.DateOfBirth;
|
||||||
|
dbUser.ActiveProfileCosmetic = request.ProfileCosmeticId;
|
||||||
|
dbUser.TextStatus = request.TextStatus;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var infoDto = new UserInformationDto
|
||||||
|
{
|
||||||
|
Id = dbUser.Id,
|
||||||
|
Username = request.Username,
|
||||||
|
ProfilePicture = dbUser.ProfilePicture,
|
||||||
|
Bio = request.Bio,
|
||||||
|
Role = dbUser.Role,
|
||||||
|
DateOfBirth = request.DateOfBirth,
|
||||||
|
CreatedAt = dbUser.CreatedAt,
|
||||||
|
Status = dbUser.Status,
|
||||||
|
CurrencyAmount = dbUser.CurrencyAmount,
|
||||||
|
ProfileCosmeticId = request.ProfileCosmeticId,
|
||||||
|
TextStatus = request.TextStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = infoDto;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User not found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<string>> UpdateUserPic(string userId, IFormFile file)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<string>();
|
||||||
|
|
||||||
|
var userToUpdate = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
var cdnPath = _configuration["GeneralConfig:CDNPath"];
|
||||||
|
|
||||||
|
if (userToUpdate == null)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User Not Found.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file != null && file.Length > 0)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(cdnPath)) Directory.CreateDirectory(cdnPath!);
|
||||||
|
if (!Directory.Exists($"{cdnPath}/{userId}")) Directory.CreateDirectory($"{cdnPath}/{userId}");
|
||||||
|
|
||||||
|
if (userToUpdate.ProfilePicture != null)
|
||||||
|
await _bucketService.DeleteProfileImage(userToUpdate.Id, userToUpdate.ProfilePicture);
|
||||||
|
|
||||||
|
var fileName = $"{Guid.NewGuid()}.{file.FileName.Split('.')[1]}";
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
file.CopyTo(ms);
|
||||||
|
var result = await _bucketService.PutProfileImage(userId, fileName, ms.ToArray());
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
userToUpdate.ProfilePicture = fileName;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = fileName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// fallback to using local cdn
|
||||||
|
|
||||||
|
var filePath = Path.Combine(cdnPath ?? "./user-content", userId, fileName);
|
||||||
|
|
||||||
|
using (var stream = File.Create(filePath))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
userToUpdate.ProfilePicture = fileName;
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "Empty File.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<FileContentResult>> GetUserPic(string userId)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<FileContentResult>();
|
||||||
|
|
||||||
|
var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
var cdnPath = _configuration["GeneralConfig:CDNPath"];
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
if (user.ProfilePicture != null)
|
||||||
|
{
|
||||||
|
var response = await _bucketService.GetProfileImageBytes(user.Id, user.ProfilePicture);
|
||||||
|
if (response != null)
|
||||||
|
{
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Message = user.ProfilePicture;
|
||||||
|
serviceResponse.Data = new FileContentResult(response, "image/jpeg");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// try local cdn
|
||||||
|
|
||||||
|
if (!Directory.Exists(cdnPath))
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User Content Folder Does Not Exist Yet.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pic = Path.Combine(cdnPath, userId, user.ProfilePicture);
|
||||||
|
|
||||||
|
if (!File.Exists(pic))
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User Does Not Have A Profile Picture.";
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Message = user.ProfilePicture;
|
||||||
|
|
||||||
|
serviceResponse.Data = new FileContentResult(File.ReadAllBytes(pic), "image/jpeg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User Does Not Have A Profile Picture.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serviceResponse.Success = false;
|
||||||
|
serviceResponse.Message = "User Not Found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<UserStatusDto>> UpdateStatus(UserStatusDto request)
|
||||||
|
{
|
||||||
|
var serviceResponse = new ServiceResponse<UserStatusDto>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(x => x.Id == request.Id)!;
|
||||||
|
|
||||||
|
user.Status = request.Status;
|
||||||
|
|
||||||
|
if (user.Status == 0)
|
||||||
|
{
|
||||||
|
user.LastLogin = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
serviceResponse.Success = true;
|
||||||
|
serviceResponse.Data = request;
|
||||||
|
|
||||||
|
return serviceResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<int>> AddCurrencyToUser(string id, int amount, bool isSpinClaim)
|
||||||
|
{
|
||||||
|
var response = new ServiceResponse<int>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(e => e.Id == id);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.CurrencyAmount += amount;
|
||||||
|
if (isSpinClaim) user.LastCurrencySpin = DateTime.UtcNow;
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
response.Success = true;
|
||||||
|
response.Data = user.CurrencyAmount;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "User Not Found";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<int>> RemoveCurrencyFromUser(string id, int amount)
|
||||||
|
{
|
||||||
|
var response = new ServiceResponse<int>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(e => e.Id == id);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.CurrencyAmount -= amount;
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
response.Success = true;
|
||||||
|
response.Data = user.CurrencyAmount;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "User Not Found";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task<ServiceResponse<int>> AddStockToUser(string id, int amount)
|
||||||
|
{
|
||||||
|
var response = new ServiceResponse<int>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(e => e.Id == id);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.StockAmount += amount;
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
response.Success = true;
|
||||||
|
response.Data = user.StockAmount;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "User Not Found";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ServiceResponse<int>> RemoveStockFromUser(string id, int amount)
|
||||||
|
{
|
||||||
|
var response = new ServiceResponse<int>();
|
||||||
|
var user = _dataContext.Users.FirstOrDefault(e => e.Id == id);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.StockAmount -= amount;
|
||||||
|
await _dataContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
response.Success = true;
|
||||||
|
response.Data = user.StockAmount;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "User Not Found";
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long LongRandom(long min, long max, Random rnd)
|
||||||
|
{
|
||||||
|
long result = rnd.Next((int)(min >> 32), (int)(max >> 32));
|
||||||
|
result = result << 32;
|
||||||
|
result = result | (long)rnd.Next((int)min, (int)max);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
qtc-net-server/libman.json
Normal file
5
qtc-net-server/libman.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"defaultProvider": "cdnjs",
|
||||||
|
"libraries": []
|
||||||
|
}
|
||||||
48
qtc-net-server/qtc-net-server.csproj
Normal file
48
qtc-net-server/qtc-net-server.csproj
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>qtc_api</RootNamespace>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AWSSDK.S3" Version="4.0.11.2" />
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
<PackageReference Include="MailKit" Version="4.17.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.13.0" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
|
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.6" />
|
||||||
|
<PackageReference Include="SIPSorcery" Version="10.0.11" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.8.58" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="Resources\store.Development.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Resources\store.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Resources\ServerConfig.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
x
Reference in New Issue
Block a user