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