Compare commits
No commits in common. "master" and "main" have entirely different histories.
@ -1,30 +0,0 @@
|
||||
**/.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
21
.env.sample
@ -1,21 +0,0 @@
|
||||
# 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
63
.gitattributes
vendored
@ -1,63 +0,0 @@
|
||||
###############################################################################
|
||||
# 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
372
.gitignore
vendored
@ -1,372 +0,0 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.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
|
||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 QtC.NET
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
21
LICENSE.txt
21
LICENSE.txt
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
34
README.md
34
README.md
@ -1,33 +1,3 @@
|
||||
# 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
|
||||
# qtcnet-server
|
||||
|
||||
# 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.
|
||||
The Backend For QtC.NET
|
||||
@ -1,16 +0,0 @@
|
||||
<?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>
|
||||
@ -1,60 +0,0 @@
|
||||
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:
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Docker Compose": {
|
||||
"commandName": "DockerCompose",
|
||||
"commandVersion": "1.0",
|
||||
"serviceActions": {
|
||||
"qtc-net-server": "StartDebugging"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
|
||||
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
|
||||
@ -1,307 +0,0 @@
|
||||
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")]
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,217 +0,0 @@
|
||||
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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
#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"]
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Dtos.Room
|
||||
{
|
||||
public class RoomDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = new DateTime();
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Dtos.User
|
||||
{
|
||||
public class UserConnectionDto
|
||||
{
|
||||
public Models.User? ConnectedUser { get; set; }
|
||||
public string? ConnectionId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Dtos.User
|
||||
{
|
||||
public class UserPasswordResetDto
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Dtos.User
|
||||
{
|
||||
public class UserStatusDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public int Status { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Dtos.User
|
||||
{
|
||||
public class UserStockActionResultDto
|
||||
{
|
||||
public int StockAmount { get; set; }
|
||||
public int CurrencyAmount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
namespace qtc_api.Enums
|
||||
{
|
||||
public enum GameStatus
|
||||
{
|
||||
WaitingForPlayer,
|
||||
SelectingSymbol,
|
||||
Ongoing,
|
||||
P1Win,
|
||||
P2Win,
|
||||
NoWin,
|
||||
PlayerDisconnected
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Enums
|
||||
{
|
||||
public enum StoreItemType
|
||||
{
|
||||
ProfileCosmetic = 1,
|
||||
ClientCosmetic = 2
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
namespace qtc_api.Enums
|
||||
{
|
||||
public enum TicTacToeSymbol
|
||||
{
|
||||
X,
|
||||
O,
|
||||
Blank
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,233 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,272 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
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 = false;
|
||||
});
|
||||
|
||||
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.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());
|
||||
|
||||
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
||||
{
|
||||
var jwtIssuer = builder.Configuration["Jwt:Issuer"];
|
||||
var jwtAudience = builder.Configuration["Jwt:Audience"];
|
||||
var jwtKey = builder.Configuration["Jwt:Key"] ?? Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
|
||||
// check configuration for invalid jwt settings
|
||||
if (jwtIssuer is null || jwtAudience is null || jwtKey is null)
|
||||
throw new Exception("JWT Validation Parameters Are Not Set In appsettings.json. Please Set These Parameters.");
|
||||
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtIssuer,
|
||||
ValidAudience = jwtAudience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||
};
|
||||
});
|
||||
|
||||
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();
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": 1,
|
||||
"Type": 1,
|
||||
"Price": 100,
|
||||
"Name": "Example",
|
||||
"Description": "Change Me!",
|
||||
"AssetUrl": "https://cdn.qtchat.net/qtc/cosmetics/test/test.gif",
|
||||
"ThumbnailUrl": "https://cdn.qtchat.net/qtc/cosmetics/test/thumbnail.jpg"
|
||||
}
|
||||
]
|
||||
@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
namespace qtc_api.Schema
|
||||
{
|
||||
public class TicTacToeMove
|
||||
{
|
||||
public User User { get; set; } = new();
|
||||
public int Point { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,157 +0,0 @@
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
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" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,309 +0,0 @@
|
||||
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);
|
||||
|
||||
var existingToken = _dataContext.ValidRefreshTokens.FirstOrDefault(e => e.UserID == refToken.UserID);
|
||||
if (existingToken != null)
|
||||
{
|
||||
refToken.Expires = existingToken.Expires;
|
||||
_dataContext.ValidRefreshTokens.Remove(existingToken); // we don't want multiple refresh tokens assigned to the user
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!result.ClaimsIdentity.HasClaim("TokenType", "access"))
|
||||
{
|
||||
serviceResponse.Success = false;
|
||||
serviceResponse.Message = "Token Invalid.";
|
||||
return serviceResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
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>();
|
||||
|
||||
var jwtIssuer = _configuration["Jwt:Issuer"];
|
||||
var jwtAudience = _configuration["Jwt:Audience"];
|
||||
var jwtKey = _configuration["Jwt:Key"] ?? Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
|
||||
// check configuration for invalid jwt settings
|
||||
if (jwtIssuer is null || jwtAudience is null || jwtKey is null)
|
||||
throw new Exception("JWT Validation Parameters Are Not Set In appsettings.json. Please Set These Parameters.");
|
||||
|
||||
serviceResponse.Data = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtIssuer,
|
||||
ValidAudience = jwtAudience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,507 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"defaultProvider": "cdnjs",
|
||||
"libraries": []
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
<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