commit 16c9aae729cc7ec6922ac3c3765a8bebeb131e27 Author: AlanMoonbase Date: Sun Jun 15 14:24:53 2025 -0700 Initial Commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -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/** \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4cac43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,366 @@ +## 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbc311e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# qtc-net-server \ No newline at end of file diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..2e8bbb3 --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,19 @@ + + + + 2.1 + Linux + False + b1b3d2c9-92e6-4711-842e-c940a40a9a2e + LaunchBrowser + {Scheme}://localhost:{ServicePort}/swagger + qtc-net-server + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..2cf6aea --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,9 @@ +version: '3.4' + +services: + qtc-net-server: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_HTTP_PORTS=8080 + ports: + - "8080" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..094684a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + qtc-net-server: + image: ${DOCKER_REGISTRY-}qtcnetserver + build: + context: . + dockerfile: qtc-net-server/Dockerfile diff --git a/launchSettings.json b/launchSettings.json new file mode 100644 index 0000000..996a0bc --- /dev/null +++ b/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "qtc-net-server": "StartDebugging" + } + } + } +} \ No newline at end of file diff --git a/qtc-net-server.sln b/qtc-net-server.sln new file mode 100644 index 0000000..9877585 --- /dev/null +++ b/qtc-net-server.sln @@ -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 diff --git a/qtc-net-server/Controllers/AuthController.cs b/qtc-net-server/Controllers/AuthController.cs new file mode 100644 index 0000000..d792287 --- /dev/null +++ b/qtc-net-server/Controllers/AuthController.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using qtc_api.Dtos.User; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace qtc_api.Controllers +{ + [Route("api/auth")] + [ApiController] + public class AuthController : ControllerBase + { + private readonly IUserService _userService; + private readonly ITokenService _tokenService; + + private readonly ServerConfig serverConfig; + private readonly DataContext dataContext; + + public AuthController(IUserService userService, ITokenService tokenService, DataContext dataContext) + { + _userService = userService; + _tokenService = tokenService; + + serverConfig = JsonSerializer.Deserialize(JsonDocument.Parse(System.IO.File.ReadAllText("./ServerConfig.json"))); + this.dataContext = dataContext; + } + + [HttpPost("register")] + public async Task>> Register(UserDto userDto) + { + if(userDto != null) + { + var response = await _userService.AddUser(userDto); + if(response.Success != false) + { + return Ok(response); + } else + { + return StatusCode(500, response.Message); + } + } else + { + return BadRequest(); + } + } + + [HttpPost("login")] + public async Task>> Login(UserLoginDto request) + { + var dbUser = await _userService.GetUserByEmail(request.Email); + + if (dbUser.Data == null) + { + return Ok(new ServiceResponse + { + Message = "User not found.", + Success = false + }); + } else if(!BCrypt.Net.BCrypt.Verify(request.Password, dbUser.Data.PasswordHash)) + { + return Ok(new ServiceResponse + { + Message = "Incorrect password.", + Success = false + }); + } + + if (dbUser.Data.Id == serverConfig.AdminUserId && dbUser.Data.Role != "Admin") + { + dbUser.Data.Role = "Admin"; + dataContext.SaveChanges(); + } + + if (dbUser.Data.Status == 1) + { + return Ok(new ServiceResponse + { + Message = "User is already signed in.", + Success = false + }); + } + + var token = await _tokenService.GenerateAccessTokenAndRefreshToken(dbUser.Data, true, request.RememberMe); + + return Ok(token); + } + + [HttpPost("refresh")] + public async Task>> RefreshLogin(string token) + { + var response = await _tokenService.ValidateRefreshToken(token); + return Ok(response); + } + } +} diff --git a/qtc-net-server/Controllers/ContactController.cs b/qtc-net-server/Controllers/ContactController.cs new file mode 100644 index 0000000..931a40f --- /dev/null +++ b/qtc-net-server/Controllers/ContactController.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using qtc_api.Models; +using qtc_api.Services.ContactService; +using System.Security.Claims; + +namespace qtc_api.Controllers +{ + [Route("api/contacts")] + [ApiController] + public class ContactController : ControllerBase + { + private IContactService _contactService; + + public ContactController(IContactService contactService) + { + _contactService = contactService; + } + + [HttpPost("add-contact")] + [Authorize] + public async Task>> AddContact(string userId) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var ownerId = claims.First().Value; + + var result = await _contactService.CreateContact(ownerId, userId); + + return Ok(result); + } + + return Unauthorized(); + } + + [HttpPost("approve-contact")] + [Authorize] + public async Task>> ApproveContact(string userId) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var ownerId = claims.First().Value; + + var result = await _contactService.UpdateContactStatus(ownerId, userId, Contact.ContactStatus.Accepted, Contact.ContactStatus.Accepted); + return Ok(result); + } + + return Unauthorized(); + } + + [HttpDelete("remove-contact")] + [Authorize] + public async Task>> RemoveContact(string userId) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var ownerId = claims.First().Value; + + var response = await _contactService.DeleteContact(ownerId, userId); + return Ok(response); + } + + return Unauthorized(); + } + + [HttpGet("get-user-contacts")] + [Authorize] + public async Task>>> GetUserContacts(User user) + { + var result = await _contactService.GetUserContacts(user); + return Ok(result); + } + } +} diff --git a/qtc-net-server/Controllers/GeneralController.cs b/qtc-net-server/Controllers/GeneralController.cs new file mode 100644 index 0000000..e18b314 --- /dev/null +++ b/qtc-net-server/Controllers/GeneralController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace qtc_api.Controllers +{ + [Route("api/general")] + [ApiController] + public class GeneralController : ControllerBase + { + private readonly ILogger logger; + public GeneralController(ILogger logger) => this.logger = logger; + + [HttpGet("ping")] + public ActionResult PingAsync() + { + logger.LogInformation($"Ping Received From Client"); + return Ok("Pong!"); + } + } +} diff --git a/qtc-net-server/Controllers/RoomsController.cs b/qtc-net-server/Controllers/RoomsController.cs new file mode 100644 index 0000000..d91ae47 --- /dev/null +++ b/qtc-net-server/Controllers/RoomsController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using qtc_api.Services.RoomService; + +namespace qtc_api.Controllers +{ + [Route("api/rooms")] + [ApiController] + public class RoomsController : ControllerBase + { + public IRoomService _roomService; + public IHubContext _hubContext; + + public RoomsController(IRoomService roomService, IHubContext hubContext) + { + _roomService = roomService; + _hubContext = hubContext; + } + + [HttpPost("create-room")] + [Authorize(Roles = "Admin")] + public async Task>> CreateRoom(string userId, RoomDto request) + { + var response = await _roomService.AddRoom(userId, request); + await _hubContext.Clients.All.SendAsync("cf", "rul"); + return Ok(response); + } + + [HttpDelete("delete-room")] + [Authorize(Roles = "Admin")] + public async Task>> DeleteRoom(string roomId) + { + var response = await _roomService.DeleteRoom(roomId); + await _hubContext.Clients.All.SendAsync("cf", "rul"); + return Ok(response); + } + + [HttpGet("get-all-rooms")] + [Authorize] + public async Task>>> GetAllRooms() + { + var rooms = await _roomService.GetAllRooms(); + return Ok(rooms); + } + } +} diff --git a/qtc-net-server/Controllers/UsersController.cs b/qtc-net-server/Controllers/UsersController.cs new file mode 100644 index 0000000..3a40064 --- /dev/null +++ b/qtc-net-server/Controllers/UsersController.cs @@ -0,0 +1,156 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using qtc_api.Dtos.User; +using System.Net.Mime; +using System.Security.Claims; +using System.Text.Json; + +namespace qtc_api.Controllers +{ + [Route("api/users")] + [ApiController] + public class UsersController : ControllerBase + { + private readonly IUserService _userService; + private readonly IConfiguration _configuration; + + public UsersController(IUserService userService, IConfiguration configuration) + { + _userService = userService; + _configuration = configuration; + } + + [HttpGet("all")] + [Authorize] + public async Task>>> GetAllUsers() + { + var users = await _userService.GetAllUsers(); + return Ok(users); + } + + [HttpGet("user-info")] + [Authorize] + public async Task>> GetUserInformation(string id) + { + var user = await _userService.GetUserInformationById(id); + return Ok(user); + } + + [HttpGet("user-authorized")] + [Authorize] + public async Task>> UserFromAuthorizeHead() + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if(identity != null) + { + IEnumerable 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."); + } + } + + [HttpGet("users-online")] + [Authorize] + public async Task>>> GetAllOnlineUsers() + { + var users = await _userService.GetAllOnlineUsers(); + return Ok(users); + } + + [HttpPut("update")] + [Authorize] + public async Task>> UpdateUserInformation(UserUpdateInformationDto user) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if(identity != null) + { + IEnumerable claims = identity.Claims; + var id = claims.First().Value; + + if(id != null && id == user.Id) + { + var updatedUser = await _userService.UpdateUserInfo(user); + return Ok(updatedUser); + } else + { + return Unauthorized("You are not authorized to edit that user."); + } + } else + { + return BadRequest("Session Expired."); + } + } + + [HttpPost("upload-profile-pic")] + [Authorize] + public async Task>> UploadOrUpdateProfilePic(string userId, IFormFile file) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if(identity != null) + { + IEnumerable 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); + + 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 GetUserProfilePicture(string userId) + { + var result = await _userService.GetUserPic(userId); + + 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>> DeleteUserById(string id) + { + var result = await _userService.DeleteUser(id); + return Ok(result); + } + } +} diff --git a/qtc-net-server/Data/DataContext.cs b/qtc-net-server/Data/DataContext.cs new file mode 100644 index 0000000..8e5ab31 --- /dev/null +++ b/qtc-net-server/Data/DataContext.cs @@ -0,0 +1,42 @@ +namespace qtc_api.Data +{ + public class DataContext : DbContext + { + public DataContext(DbContextOptions options) : base(options) + { + } + + public DbSet Users { get; set; } + public DbSet Rooms { get; set; } + public DbSet ValidRefreshTokens { get; set; } + public DbSet Contacts { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + // Users + + builder.Entity().HasMany(e => e.ContactsList); + builder.Entity().HasMany(e => e.ContactsMade); + + // Rooms (no relations) + + builder.Entity(); + + // Refresh Tokens + + builder.Entity().HasOne(e => e.User) + .WithMany(e => e.RefreshTokens) + .HasForeignKey(e => e.UserID); + + // Contacts + + builder.Entity().HasOne(e => e.Owner) + .WithMany(e => e.ContactsMade) + .HasForeignKey(e => e.OwnerId); + + builder.Entity().HasOne(e => e.User) + .WithMany(e => e.ContactsList) + .HasForeignKey(e => e.UserId); + } + } +} diff --git a/qtc-net-server/Dockerfile b/qtc-net-server/Dockerfile new file mode 100644 index 0000000..eb1ae0d --- /dev/null +++ b/qtc-net-server/Dockerfile @@ -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:8.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:8.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"] \ No newline at end of file diff --git a/qtc-net-server/Dtos/Room/RoomDto.cs b/qtc-net-server/Dtos/Room/RoomDto.cs new file mode 100644 index 0000000..6c24370 --- /dev/null +++ b/qtc-net-server/Dtos/Room/RoomDto.cs @@ -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(); + } +} diff --git a/qtc-net-server/Dtos/User/UserConnectionDto.cs b/qtc-net-server/Dtos/User/UserConnectionDto.cs new file mode 100644 index 0000000..bb44c23 --- /dev/null +++ b/qtc-net-server/Dtos/User/UserConnectionDto.cs @@ -0,0 +1,8 @@ +namespace qtc_api.Dtos.User +{ + public class UserConnectionDto + { + public Models.User? ConnectedUser { get; set; } + public string? ConnectionId { get; set; } + } +} diff --git a/qtc-net-server/Dtos/User/UserDto.cs b/qtc-net-server/Dtos/User/UserDto.cs new file mode 100644 index 0000000..1d51b7a --- /dev/null +++ b/qtc-net-server/Dtos/User/UserDto.cs @@ -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(); + } +} diff --git a/qtc-net-server/Dtos/User/UserInformationDto.cs b/qtc-net-server/Dtos/User/UserInformationDto.cs new file mode 100644 index 0000000..0793483 --- /dev/null +++ b/qtc-net-server/Dtos/User/UserInformationDto.cs @@ -0,0 +1,14 @@ +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 int Status { get; set; } = 0; + } +} diff --git a/qtc-net-server/Dtos/User/UserLoginDto.cs b/qtc-net-server/Dtos/User/UserLoginDto.cs new file mode 100644 index 0000000..bd72684 --- /dev/null +++ b/qtc-net-server/Dtos/User/UserLoginDto.cs @@ -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; + } +} diff --git a/qtc-net-server/Dtos/User/UserStatusDto.cs b/qtc-net-server/Dtos/User/UserStatusDto.cs new file mode 100644 index 0000000..591f9c1 --- /dev/null +++ b/qtc-net-server/Dtos/User/UserStatusDto.cs @@ -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; + } +} diff --git a/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs new file mode 100644 index 0000000..a97cc3d --- /dev/null +++ b/qtc-net-server/Dtos/User/UserUpdateInformationDto.cs @@ -0,0 +1,10 @@ +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(); + } +} diff --git a/qtc-net-server/Hubs/ChatHub.cs b/qtc-net-server/Hubs/ChatHub.cs new file mode 100644 index 0000000..98170ce --- /dev/null +++ b/qtc-net-server/Hubs/ChatHub.cs @@ -0,0 +1,167 @@ +using System.Text.Json; + +namespace qtc_gateway.Hubs +{ + [Authorize] + public class ChatHub : Hub + { + private IUserService _userService; + private ILogger _logger; + private static List ConnectedUsers = new List(); + private static List OnlineUsers = new List(); + + public ChatHub(IUserService userService, ILogger logger) + { + _userService = userService; + _logger = logger; + } + + public override async Task OnDisconnectedAsync(Exception? ex) + { + Log("Client Disconnected."); + + var connection = ConnectedUsers.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId); + + if (connection != null) + { + var user = OnlineUsers.FirstOrDefault(x => x.Id == connection!.ConnectedUser!.Id); + + if (user != null) + { + await LogoutAsync(user!); + } + } + } + + [HubMethodName("l")] + public async Task LoginAsync(User user) + { + await Clients.All.SendAsync("rm", $"[SERVER] User {user.Username} Is Now Online"); + Log($"User {user.Username} Has Logged In"); + + var statusDto = new UserStatusDto { Id = user.Id, Status = 1 }; + + await _userService.UpdateStatus(statusDto); + + ConnectedUsers.Add(new UserConnectionDto() { ConnectedUser = user, ConnectionId = Context.ConnectionId }); + OnlineUsers.Add(user); + + ServerConfig serverConfig = JsonSerializer.Deserialize(JsonDocument.Parse(File.ReadAllText("./ServerConfig.json"))); + + await Clients.Client(ConnectedUsers.FirstOrDefault(e => e.ConnectionId == Context.ConnectionId)!.ConnectionId!).SendAsync("rc", serverConfig); + await Clients.All.SendAsync("cf", "rul"); + } + + [HubMethodName("us")] + public async Task UpdateStatusAsync(User user, int status) + { + var statusDto = new UserStatusDto { Id = user.Id, Status = status }; + + await _userService.UpdateStatus(statusDto); + + await Clients.All.SendAsync("cf", "rul"); + } + + [HubMethodName("jl")] + public async Task JoinHubAsync(User user) + { + await Groups.AddToGroupAsync(Context.ConnectionId, "LOBBY"); + + await Clients.Group("LOBBY").SendAsync("rm", $"[SERVER] User {user.Username} Has Joined The Lobby"); + Log($"User {user.Username} Has Joined The Lobby"); + } + + [HubMethodName("ll")] + public async Task LeaveLobbyAsync(User user) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, "LOBBY"); + + await Clients.Group("LOBBY").SendAsync("rm", $"[SERVER] User {user.Username} Has Left The Lobby"); + Log($"User {user.Username} Has Left The Lobby"); + } + + [HubMethodName("jr")] + public async Task JoinRoomAsync(User user, Room room) + { + await Groups.AddToGroupAsync(Context.ConnectionId, room.Id); + + await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] User {user.Username} Has Joined The Room"); + Log($"User {user.Username} Has Joined {room.Name}"); + } + + [HubMethodName("lr")] + public async Task LeaveRoomAsync(User user, Room room) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id); + + await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] User {user.Username} Has Left The Room"); + Log($"User {user.Username} Has Left {room.Name}"); + } + + [HubMethodName("hdr")] + public async Task HandleDeletedRoomAsync(Room room) + { + await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] This Room Has Been Deleted By An Administrator."); + await Clients.Group(room.Id).SendAsync("cf", "rtl"); + + await Clients.All.SendAsync("cf", "rr"); + } + + [HubMethodName("rcl")] + public async Task RefreshContactsListForUser(UserInformationDto user, User execUser) + { + var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == user.Id); + var connection2 = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == execUser.Id); + if (connection != null && connection2 != null) + { + await Clients.Client(connection.ConnectionId).SendAsync("cf", "rcl"); + await Clients.Client(connection2.ConnectionId).SendAsync("cf", "rcl"); + return; + } + } + + [HubMethodName("s")] + public async Task SendMessageAsync(User user, Message message, bool IsLobbyMsg, Room room = null!) + { + if(IsLobbyMsg == true) { await Clients.Group("LOBBY").SendAsync("rm", $"[{user.Username}] {message.Content}"); return; } + await Clients.Group(room.Id).SendAsync("rm", $"[{user.Username}] {message.Content}"); + } + + [HubMethodName("sdm")] + public async Task SendDirectMessageAsync(User user, UserInformationDto userToMsg, Message message) + { + // send direct message directly to connected user + var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == userToMsg.Id); + 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 }; + await Clients.Client(connection.ConnectionId).SendAsync("rdm", message, userInformationDto); + return; + } + } + + private async Task LogoutAsync(User user) + { + await Clients.All.SendAsync("rm", $"[SERVER] User {user.Username} Is Now Offline"); + Log($"User {user.Username} Has Logged Out"); + + var statusDto = new UserStatusDto { Id = user.Id, Status = 0 }; + + await _userService.UpdateStatus(statusDto); + + Log($"Set User {user.Username} To Offline"); + + await Clients.All.SendAsync("cf", "rul"); + + OnlineUsers.Remove(user); + + var connection = ConnectedUsers.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId); + ConnectedUsers.Remove(connection!); + } + + private void Log(string message) + { + _logger.LogInformation(message); + } + } +} diff --git a/qtc-net-server/Migrations/20250605232504_InitialData.Designer.cs b/qtc-net-server/Migrations/20250605232504_InitialData.Designer.cs new file mode 100644 index 0000000..cb877b3 --- /dev/null +++ b/qtc-net-server/Migrations/20250605232504_InitialData.Designer.cs @@ -0,0 +1,133 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250605232504_InitialData")] + partial class InitialData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ID"); + + b.ToTable("ValidRefreshTokens"); + }); + + modelBuilder.Entity("qtc_api.Models.Room", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Migrations/20250605232504_InitialData.cs b/qtc-net-server/Migrations/20250605232504_InitialData.cs new file mode 100644 index 0000000..c3c3e44 --- /dev/null +++ b/qtc-net-server/Migrations/20250605232504_InitialData.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace qtc_api.Migrations +{ + /// + public partial class InitialData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false), + OwnerId = table.Column(type: "longtext", nullable: false), + UserId = table.Column(type: "longtext", nullable: false), + Status = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Rooms", + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false), + Name = table.Column(type: "longtext", nullable: false), + CreatorId = table.Column(type: "longtext", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Rooms", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false), + Username = table.Column(type: "longtext", nullable: false), + ProfilePicture = table.Column(type: "longtext", nullable: false), + Bio = table.Column(type: "longtext", nullable: false), + Role = table.Column(type: "longtext", nullable: false), + PasswordHash = table.Column(type: "longtext", nullable: false), + Email = table.Column(type: "longtext", nullable: false), + DateOfBirth = table.Column(type: "datetime(6)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + Status = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ValidRefreshTokens", + columns: table => new + { + ID = table.Column(type: "varchar(255)", nullable: false), + UserID = table.Column(type: "longtext", nullable: false), + Token = table.Column(type: "longtext", nullable: false), + Expires = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ValidRefreshTokens", x => x.ID); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Contacts"); + + migrationBuilder.DropTable( + name: "Rooms"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "ValidRefreshTokens"); + } + } +} diff --git a/qtc-net-server/Migrations/20250606000415_AddExtraStatus.Designer.cs b/qtc-net-server/Migrations/20250606000415_AddExtraStatus.Designer.cs new file mode 100644 index 0000000..8d655b0 --- /dev/null +++ b/qtc-net-server/Migrations/20250606000415_AddExtraStatus.Designer.cs @@ -0,0 +1,136 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250606000415_AddExtraStatus")] + partial class AddExtraStatus + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerStatus") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("ID"); + + b.ToTable("ValidRefreshTokens"); + }); + + modelBuilder.Entity("qtc_api.Models.Room", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Migrations/20250606000415_AddExtraStatus.cs b/qtc-net-server/Migrations/20250606000415_AddExtraStatus.cs new file mode 100644 index 0000000..edd5c4f --- /dev/null +++ b/qtc-net-server/Migrations/20250606000415_AddExtraStatus.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace qtc_api.Migrations +{ + /// + public partial class AddExtraStatus : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Status", + table: "Contacts", + newName: "UserStatus"); + + migrationBuilder.AddColumn( + name: "OwnerStatus", + table: "Contacts", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OwnerStatus", + table: "Contacts"); + + migrationBuilder.RenameColumn( + name: "UserStatus", + table: "Contacts", + newName: "Status"); + } + } +} diff --git a/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.Designer.cs b/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.Designer.cs new file mode 100644 index 0000000..5162924 --- /dev/null +++ b/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.Designer.cs @@ -0,0 +1,160 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250608203855_RefreshTokensForeignKey")] + partial class RefreshTokensForeignKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OwnerStatus") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("ID"); + + b.HasIndex("UserID"); + + b.ToTable("RefreshToken"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.HasOne("qtc_api.Models.User", "Owner") + .WithMany("ContactsMade") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("qtc_api.Models.User", "User") + .WithMany("ContactsList") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.HasOne("qtc_api.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Navigation("ContactsList"); + + b.Navigation("ContactsMade"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.cs b/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.cs new file mode 100644 index 0000000..34926cb --- /dev/null +++ b/qtc-net-server/Migrations/20250608203855_RefreshTokensForeignKey.cs @@ -0,0 +1,226 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace qtc_api.Migrations +{ + /// + public partial class RefreshTokensForeignKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Rooms"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ValidRefreshTokens", + table: "ValidRefreshTokens"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Contacts", + table: "Contacts"); + + migrationBuilder.RenameTable( + name: "ValidRefreshTokens", + newName: "RefreshToken"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "User"); + + migrationBuilder.RenameTable( + name: "Contacts", + newName: "Contact"); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "RefreshToken", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Contact", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Contact", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext"); + + migrationBuilder.AddPrimaryKey( + name: "PK_RefreshToken", + table: "RefreshToken", + column: "ID"); + + migrationBuilder.AddPrimaryKey( + name: "PK_User", + table: "User", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Contact", + table: "Contact", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_RefreshToken_UserID", + table: "RefreshToken", + column: "UserID"); + + migrationBuilder.CreateIndex( + name: "IX_Contact_OwnerId", + table: "Contact", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Contact_UserId", + table: "Contact", + column: "UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Contact_User_OwnerId", + table: "Contact", + column: "OwnerId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Contact_User_UserId", + table: "Contact", + column: "UserId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_RefreshToken_User_UserID", + table: "RefreshToken", + column: "UserID", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Contact_User_OwnerId", + table: "Contact"); + + migrationBuilder.DropForeignKey( + name: "FK_Contact_User_UserId", + table: "Contact"); + + migrationBuilder.DropForeignKey( + name: "FK_RefreshToken_User_UserID", + table: "RefreshToken"); + + migrationBuilder.DropPrimaryKey( + name: "PK_User", + table: "User"); + + migrationBuilder.DropPrimaryKey( + name: "PK_RefreshToken", + table: "RefreshToken"); + + migrationBuilder.DropIndex( + name: "IX_RefreshToken_UserID", + table: "RefreshToken"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Contact", + table: "Contact"); + + migrationBuilder.DropIndex( + name: "IX_Contact_OwnerId", + table: "Contact"); + + migrationBuilder.DropIndex( + name: "IX_Contact_UserId", + table: "Contact"); + + migrationBuilder.RenameTable( + name: "User", + newName: "Users"); + + migrationBuilder.RenameTable( + name: "RefreshToken", + newName: "ValidRefreshTokens"); + + migrationBuilder.RenameTable( + name: "Contact", + newName: "Contacts"); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "ValidRefreshTokens", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "Contacts", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "Contacts", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ValidRefreshTokens", + table: "ValidRefreshTokens", + column: "ID"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Contacts", + table: "Contacts", + column: "Id"); + + migrationBuilder.CreateTable( + name: "Rooms", + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "longtext", nullable: false), + Name = table.Column(type: "longtext", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Rooms", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + } + } +} diff --git a/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.Designer.cs b/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.Designer.cs new file mode 100644 index 0000000..21d0297 --- /dev/null +++ b/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.Designer.cs @@ -0,0 +1,181 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250608204114_AddRemainingRelations")] + partial class AddRemainingRelations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OwnerStatus") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("ID"); + + b.HasIndex("UserID"); + + b.ToTable("RefreshToken"); + }); + + modelBuilder.Entity("qtc_api.Models.Room", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Room"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.HasOne("qtc_api.Models.User", "Owner") + .WithMany("ContactsMade") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("qtc_api.Models.User", "User") + .WithMany("ContactsList") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.HasOne("qtc_api.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Navigation("ContactsList"); + + b.Navigation("ContactsMade"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.cs b/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.cs new file mode 100644 index 0000000..8497ef0 --- /dev/null +++ b/qtc-net-server/Migrations/20250608204114_AddRemainingRelations.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace qtc_api.Migrations +{ + /// + public partial class AddRemainingRelations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Room", + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false), + Name = table.Column(type: "longtext", nullable: false), + CreatorId = table.Column(type: "longtext", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Room", x => x.Id); + }) + .Annotation("MySQL:Charset", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Room"); + } + } +} diff --git a/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.Designer.cs b/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.Designer.cs new file mode 100644 index 0000000..4fd845e --- /dev/null +++ b/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.Designer.cs @@ -0,0 +1,181 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250608204625_ValidRefreshTokens")] + partial class ValidRefreshTokens + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OwnerStatus") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("ID"); + + b.HasIndex("UserID"); + + b.ToTable("ValidRefreshTokens"); + }); + + modelBuilder.Entity("qtc_api.Models.Room", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.HasOne("qtc_api.Models.User", "Owner") + .WithMany("ContactsMade") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("qtc_api.Models.User", "User") + .WithMany("ContactsList") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.HasOne("qtc_api.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Navigation("ContactsList"); + + b.Navigation("ContactsMade"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.cs b/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.cs new file mode 100644 index 0000000..9c4ae5a --- /dev/null +++ b/qtc-net-server/Migrations/20250608204625_ValidRefreshTokens.cs @@ -0,0 +1,224 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace qtc_api.Migrations +{ + /// + public partial class ValidRefreshTokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Contact_User_OwnerId", + table: "Contact"); + + migrationBuilder.DropForeignKey( + name: "FK_Contact_User_UserId", + table: "Contact"); + + migrationBuilder.DropForeignKey( + name: "FK_RefreshToken_User_UserID", + table: "RefreshToken"); + + migrationBuilder.DropPrimaryKey( + name: "PK_User", + table: "User"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Room", + table: "Room"); + + migrationBuilder.DropPrimaryKey( + name: "PK_RefreshToken", + table: "RefreshToken"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Contact", + table: "Contact"); + + migrationBuilder.RenameTable( + name: "User", + newName: "Users"); + + migrationBuilder.RenameTable( + name: "Room", + newName: "Rooms"); + + migrationBuilder.RenameTable( + name: "RefreshToken", + newName: "ValidRefreshTokens"); + + migrationBuilder.RenameTable( + name: "Contact", + newName: "Contacts"); + + migrationBuilder.RenameIndex( + name: "IX_RefreshToken_UserID", + table: "ValidRefreshTokens", + newName: "IX_ValidRefreshTokens_UserID"); + + migrationBuilder.RenameIndex( + name: "IX_Contact_UserId", + table: "Contacts", + newName: "IX_Contacts_UserId"); + + migrationBuilder.RenameIndex( + name: "IX_Contact_OwnerId", + table: "Contacts", + newName: "IX_Contacts_OwnerId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Rooms", + table: "Rooms", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ValidRefreshTokens", + table: "ValidRefreshTokens", + column: "ID"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Contacts", + table: "Contacts", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Contacts_Users_OwnerId", + table: "Contacts", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Contacts_Users_UserId", + table: "Contacts", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ValidRefreshTokens_Users_UserID", + table: "ValidRefreshTokens", + column: "UserID", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Contacts_Users_OwnerId", + table: "Contacts"); + + migrationBuilder.DropForeignKey( + name: "FK_Contacts_Users_UserId", + table: "Contacts"); + + migrationBuilder.DropForeignKey( + name: "FK_ValidRefreshTokens_Users_UserID", + table: "ValidRefreshTokens"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ValidRefreshTokens", + table: "ValidRefreshTokens"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Rooms", + table: "Rooms"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Contacts", + table: "Contacts"); + + migrationBuilder.RenameTable( + name: "ValidRefreshTokens", + newName: "RefreshToken"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "User"); + + migrationBuilder.RenameTable( + name: "Rooms", + newName: "Room"); + + migrationBuilder.RenameTable( + name: "Contacts", + newName: "Contact"); + + migrationBuilder.RenameIndex( + name: "IX_ValidRefreshTokens_UserID", + table: "RefreshToken", + newName: "IX_RefreshToken_UserID"); + + migrationBuilder.RenameIndex( + name: "IX_Contacts_UserId", + table: "Contact", + newName: "IX_Contact_UserId"); + + migrationBuilder.RenameIndex( + name: "IX_Contacts_OwnerId", + table: "Contact", + newName: "IX_Contact_OwnerId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_RefreshToken", + table: "RefreshToken", + column: "ID"); + + migrationBuilder.AddPrimaryKey( + name: "PK_User", + table: "User", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Room", + table: "Room", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Contact", + table: "Contact", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Contact_User_OwnerId", + table: "Contact", + column: "OwnerId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Contact_User_UserId", + table: "Contact", + column: "UserId", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_RefreshToken_User_UserID", + table: "RefreshToken", + column: "UserID", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/qtc-net-server/Migrations/DataContextModelSnapshot.cs b/qtc-net-server/Migrations/DataContextModelSnapshot.cs new file mode 100644 index 0000000..311e25c --- /dev/null +++ b/qtc-net-server/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,178 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using qtc_api.Data; + +#nullable disable + +namespace qtc_api.Migrations +{ + [DbContext(typeof(DataContext))] + partial class DataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("OwnerStatus") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UserStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.Property("ID") + .HasColumnType("varchar(255)"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("ID"); + + b.HasIndex("UserID"); + + b.ToTable("ValidRefreshTokens"); + }); + + modelBuilder.Entity("qtc_api.Models.Room", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Bio") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("qtc_api.Models.Contact", b => + { + b.HasOne("qtc_api.Models.User", "Owner") + .WithMany("ContactsMade") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("qtc_api.Models.User", "User") + .WithMany("ContactsList") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.RefreshToken", b => + { + b.HasOne("qtc_api.Models.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("qtc_api.Models.User", b => + { + b.Navigation("ContactsList"); + + b.Navigation("ContactsMade"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/qtc-net-server/Models/Contact.cs b/qtc-net-server/Models/Contact.cs new file mode 100644 index 0000000..caf4b5b --- /dev/null +++ b/qtc-net-server/Models/Contact.cs @@ -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 } + } +} diff --git a/qtc-net-server/Models/Message.cs b/qtc-net-server/Models/Message.cs new file mode 100644 index 0000000..477ec5e --- /dev/null +++ b/qtc-net-server/Models/Message.cs @@ -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(); + } +} diff --git a/qtc-net-server/Models/RefreshToken.cs b/qtc-net-server/Models/RefreshToken.cs new file mode 100644 index 0000000..804dfe0 --- /dev/null +++ b/qtc-net-server/Models/RefreshToken.cs @@ -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!; + } +} diff --git a/qtc-net-server/Models/Room.cs b/qtc-net-server/Models/Room.cs new file mode 100644 index 0000000..d3108fd --- /dev/null +++ b/qtc-net-server/Models/Room.cs @@ -0,0 +1,10 @@ +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(); + } +} diff --git a/qtc-net-server/Models/ServerConfig.cs b/qtc-net-server/Models/ServerConfig.cs new file mode 100644 index 0000000..190b5f2 --- /dev/null +++ b/qtc-net-server/Models/ServerConfig.cs @@ -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; } + } +} diff --git a/qtc-net-server/Models/ServiceResponse.cs b/qtc-net-server/Models/ServiceResponse.cs new file mode 100644 index 0000000..6fca883 --- /dev/null +++ b/qtc-net-server/Models/ServiceResponse.cs @@ -0,0 +1,9 @@ +namespace qtc_api.Models +{ + public class ServiceResponse + { + public T? Data { get; set; } + public bool Success { get; set; } = false; + public string Message { get; set; } = string.Empty; + } +} diff --git a/qtc-net-server/Models/User.cs b/qtc-net-server/Models/User.cs new file mode 100644 index 0000000..39cb98c --- /dev/null +++ b/qtc-net-server/Models/User.cs @@ -0,0 +1,20 @@ +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 DateTime DateOfBirth { get; set; } + public DateTime CreatedAt { get; set; } + public int Status { get; set; } = 0; + + public virtual IEnumerable? RefreshTokens { get; } + public virtual IEnumerable? ContactsMade { get; } + public virtual IEnumerable? ContactsList { get; } + } +} diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs new file mode 100644 index 0000000..2538ce0 --- /dev/null +++ b/qtc-net-server/Program.cs @@ -0,0 +1,63 @@ +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.AspNetCore.SignalR; +global using Microsoft.AspNetCore.Authorization; +global using qtc_api.Models; +global using qtc_api.Data; +global using qtc_api.Dtos.User; +global using qtc_api.Dtos.Room; +global using qtc_api.Services.UserService; +global using Microsoft.IdentityModel.Tokens; +global using System.Text; +global using qtc_api.Services.TokenService; +global using qtc_gateway.Hubs; +using qtc_api.Services.RoomService; +using qtc_api.Services.ContactService; +using Microsoft.EntityFrameworkCore.Diagnostics; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddDbContext(options => +{ + if (builder.Environment.IsProduction()) options.UseMySQL(builder.Configuration.GetConnectionString("DefaultConnection")); + else options.UseSqlite(builder.Configuration.GetConnectionString("DevelopmentConnection")); + + // ignore pending model changes warning + options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); +}); +builder.Services.AddSignalR(); + +builder.Services.AddAuthentication().AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) + }; +}); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +using var scope = app.Services.CreateScope(); + +await scope.ServiceProvider.GetRequiredService().Database.EnsureCreatedAsync(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.MapHub("/chat"); + +app.Run(); diff --git a/qtc-net-server/Properties/launchSettings.json b/qtc-net-server/Properties/launchSettings.json new file mode 100644 index 0000000..41bdace --- /dev/null +++ b/qtc-net-server/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5268" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:26136", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/qtc-net-server/ServerConfig.json b/qtc-net-server/ServerConfig.json new file mode 100644 index 0000000..fb35ef6 --- /dev/null +++ b/qtc-net-server/ServerConfig.json @@ -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." +} \ No newline at end of file diff --git a/qtc-net-server/Services/ContactService/ContactService.cs b/qtc-net-server/Services/ContactService/ContactService.cs new file mode 100644 index 0000000..e109f2e --- /dev/null +++ b/qtc-net-server/Services/ContactService/ContactService.cs @@ -0,0 +1,118 @@ +namespace qtc_api.Services.ContactService +{ + public class ContactService : IContactService + { + private readonly DataContext _dataContext; + + public ContactService(DataContext dataContext) + { + _dataContext = dataContext; + } + + public async Task> CreateContact(string ownerId, string userId) + { + var serviceResponse = new ServiceResponse(); + 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> UpdateContactStatus(string ownerId, string userId, Contact.ContactStatus ownerStatus, Contact.ContactStatus userStatus) + { + var serviceResponse = new ServiceResponse(); + 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> DeleteContact(string ownerId, string userId) + { + var serviceResponse = new ServiceResponse(); + var contact = await _dataContext.Contacts.FirstOrDefaultAsync(x => (x.OwnerId == ownerId || x.UserId == userId) || (x.OwnerId == userId || x.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> GetContactById(string id) + { + var serviceResponse = new ServiceResponse(); + + 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>> GetUserContacts(User user) + { + var serviceResponse = new ServiceResponse>(); + + 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(); + + 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; + } + } +} diff --git a/qtc-net-server/Services/ContactService/IContactService.cs b/qtc-net-server/Services/ContactService/IContactService.cs new file mode 100644 index 0000000..bcbe441 --- /dev/null +++ b/qtc-net-server/Services/ContactService/IContactService.cs @@ -0,0 +1,11 @@ +namespace qtc_api.Services.ContactService +{ + public interface IContactService + { + public Task> CreateContact(string ownerId, string userId); + public Task> UpdateContactStatus(string ownerId, string userId, Contact.ContactStatus ownerStatus, Contact.ContactStatus userStatus); + public Task>> GetUserContacts(User user); + public Task> GetContactById(string id); + public Task> DeleteContact(string ownerId, string userId); + } +} diff --git a/qtc-net-server/Services/RoomService/IRoomService.cs b/qtc-net-server/Services/RoomService/IRoomService.cs new file mode 100644 index 0000000..36e3338 --- /dev/null +++ b/qtc-net-server/Services/RoomService/IRoomService.cs @@ -0,0 +1,11 @@ +namespace qtc_api.Services.RoomService +{ + public interface IRoomService + { + public Task> GetRoom(string id); + public Task>> GetAllRooms(); + public Task> AddRoom(string userId, RoomDto room); + public Task> UpdateRoom(Room room); + public Task> DeleteRoom(string id); + } +} diff --git a/qtc-net-server/Services/RoomService/RoomService.cs b/qtc-net-server/Services/RoomService/RoomService.cs new file mode 100644 index 0000000..8512239 --- /dev/null +++ b/qtc-net-server/Services/RoomService/RoomService.cs @@ -0,0 +1,107 @@ +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> AddRoom(string userId, RoomDto room) + { + var serviceResponse = new ServiceResponse(); + 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> DeleteRoom(string id) + { + var serviceResponse = new ServiceResponse(); + 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>> GetAllRooms() + { + var serviceResponse = new ServiceResponse>(); + var rooms = await _dataContext.Rooms.ToListAsync(); + + serviceResponse.Success = true; + serviceResponse.Data = rooms; + return serviceResponse; + } + + public async Task> GetRoom(string id) + { + var serviceResponse = new ServiceResponse(); + 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> 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; + } + } +} diff --git a/qtc-net-server/Services/TokenService/ITokenService.cs b/qtc-net-server/Services/TokenService/ITokenService.cs new file mode 100644 index 0000000..6d0b67a --- /dev/null +++ b/qtc-net-server/Services/TokenService/ITokenService.cs @@ -0,0 +1,10 @@ +namespace qtc_api.Services.TokenService +{ + public interface ITokenService + { + public Task> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false); + public Task> ValidateAccessToken(string accessToken); + public Task> ValidateRefreshToken(string refreshToken); + public ServiceResponse GetValidationParams(); + } +} diff --git a/qtc-net-server/Services/TokenService/TokenService.cs b/qtc-net-server/Services/TokenService/TokenService.cs new file mode 100644 index 0000000..cedcd38 --- /dev/null +++ b/qtc-net-server/Services/TokenService/TokenService.cs @@ -0,0 +1,176 @@ +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> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken, bool remember) + { + var serviceResponse = new ServiceResponse(); + + // Generate JWT Access Token + + List claims = new List() + { + new Claim(ClaimTypes.Hash, user.Id), + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value!)); + 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.AddDays(7); + else refToken.Expires = DateTime.UtcNow.AddDays(1); + + _dataContext.ValidRefreshTokens.Add(refToken); + + await _dataContext.SaveChangesAsync(); + + serviceResponse.Message = refToken.Token; + } + + serviceResponse.Success = true; + return serviceResponse; + } + + public async Task> ValidateRefreshToken(string refreshToken) + { + var serviceResponse = new ServiceResponse(); + + var dbRefresh = await _dataContext.ValidRefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken); + + if (dbRefresh != null) + { + if (dbRefresh.Expires < DateTime.UtcNow) + { + 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, false, false); + + if (token != null) + { + serviceResponse.Success = true; + 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> ValidateAccessToken(string accessToken) + { + var serviceResponse = new ServiceResponse(); + + 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 GetValidationParams() + { + var serviceResponse = new ServiceResponse(); + + 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; + } + } +} diff --git a/qtc-net-server/Services/UserService/IUserService.cs b/qtc-net-server/Services/UserService/IUserService.cs new file mode 100644 index 0000000..d65e2f6 --- /dev/null +++ b/qtc-net-server/Services/UserService/IUserService.cs @@ -0,0 +1,19 @@ +using qtc_api.Dtos.User; + +namespace qtc_api.Services.UserService +{ + public interface IUserService + { + public Task> GetUserById(string id); + public Task> GetUserInformationById(string id); + public Task> GetUserByEmail(string email); + public Task>> GetAllUsers(); + public Task>> GetAllOnlineUsers(); + public Task> AddUser(UserDto userReq); + public Task> UpdateUserInfo(UserUpdateInformationDto request); + public Task> UpdateUserPic(string userId, IFormFile file); + public Task> GetUserPic(string userId); + public Task> UpdateStatus(UserStatusDto request); + public Task> DeleteUser(string id); + } +} diff --git a/qtc-net-server/Services/UserService/UserService.cs b/qtc-net-server/Services/UserService/UserService.cs new file mode 100644 index 0000000..7954e9a --- /dev/null +++ b/qtc-net-server/Services/UserService/UserService.cs @@ -0,0 +1,340 @@ +namespace qtc_api.Services.UserService +{ + public class UserService : IUserService + { + private readonly IConfiguration _configuration; + private readonly DataContext _dataContext; + + private long idMax = 900000000000000000; + + public UserService(IConfiguration configuration, DataContext dataContext) + { + _configuration = configuration; + _dataContext = dataContext; + } + + public async Task> AddUser(UserDto userReq) + { + var serviceResponse = new ServiceResponse(); + 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; + + 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> DeleteUser(string id) + { + var serviceResponse = new ServiceResponse(); + 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>> GetAllUsers() + { + var serviceResponse = new ServiceResponse>(); + var userList = await _dataContext.Users.ToListAsync(); + + var userInfoList = new List(); + 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; + + userInfoList.Add(x); + } + + serviceResponse.Success = true; + serviceResponse.Data = userInfoList; + return serviceResponse; + } + + public async Task>> GetAllOnlineUsers() + { + var serviceResponse = new ServiceResponse>(); + var onlineUsers = new List(); + + 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; + + onlineUsers.Add(x); + } + }); + + serviceResponse.Success = true; + serviceResponse.Data = onlineUsers; + return serviceResponse; + } + + public async Task> GetUserById(string id) + { + var serviceResponse = new ServiceResponse(); + var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == id); + serviceResponse.Success = true; + serviceResponse.Data = user; + return serviceResponse; + } + + public async Task> GetUserInformationById(string id) + { + var serviceResponse = new ServiceResponse(); + var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == id); + + if(user != null) + { + var dto = new UserInformationDto(); + + dto.Id = user.Id; + dto.Username = user.Username; + dto.ProfilePicture = user.ProfilePicture; + dto.Role = user.Role; + dto.Bio = user.Bio; + dto.DateOfBirth = user.DateOfBirth; + dto.CreatedAt = user.CreatedAt; + dto.Status = user.Status; + + serviceResponse.Success = true; + serviceResponse.Data = dto; + return serviceResponse; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "User not found."; + return serviceResponse; + } + } + + public async Task> GetUserByEmail(string email) + { + var serviceResponse = new ServiceResponse(); + var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Email == email); + serviceResponse.Success = true; + serviceResponse.Data = user; + return serviceResponse; + } + + public async Task> UpdateUserInfo(UserUpdateInformationDto request) + { + var serviceResponse = new ServiceResponse(); + 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; + + await _dataContext.SaveChangesAsync(); + + var infoDto = new UserInformationDto(); + + infoDto.Id = dbUser.Id; + infoDto.Username = request.Username; + infoDto.ProfilePicture = dbUser.ProfilePicture; + infoDto.Bio = request.Bio; + infoDto.Role = dbUser.Role; + infoDto.DateOfBirth = request.DateOfBirth; + infoDto.CreatedAt = dbUser.CreatedAt; + infoDto.Status = dbUser.Status; + + serviceResponse.Success = true; + serviceResponse.Data = infoDto; + } else + { + serviceResponse.Success = false; + serviceResponse.Message = "User not found."; + } + + return serviceResponse; + } + + public async Task> UpdateUserPic(string userId, IFormFile file) + { + var serviceResponse = new ServiceResponse(); + + 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}"); + + var fileName = $"{userId}.{file.FileName.Split('.')[1]}"; + var filePath = Path.Combine(cdnPath!, 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> GetUserPic(string userId) + { + var serviceResponse = new ServiceResponse(); + + var user = await _dataContext.Users.FirstOrDefaultAsync(x => x.Id == userId); + var cdnPath = _configuration["GeneralConfig:CDNPath"]; + + if (user != null) + { + if (user.ProfilePicture != null) + { + 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/{Path.GetExtension(pic)}"); + } 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> UpdateStatus(UserStatusDto request) + { + var serviceResponse = new ServiceResponse(); + var user = _dataContext.Users.FirstOrDefault(x => x.Id == request.Id)!; + + user.Status = request.Status; + + await _dataContext.SaveChangesAsync(); + + serviceResponse.Success = true; + serviceResponse.Data = request; + + 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; + } + } +} diff --git a/qtc-net-server/appsettings.Development.json b/qtc-net-server/appsettings.Development.json new file mode 100644 index 0000000..43b43db --- /dev/null +++ b/qtc-net-server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Debug" + } + } +} diff --git a/qtc-net-server/appsettings.json b/qtc-net-server/appsettings.json new file mode 100644 index 0000000..3790a2b --- /dev/null +++ b/qtc-net-server/appsettings.json @@ -0,0 +1,22 @@ +{ + "Jwt": { + "Key": "bgpLLhY2L2UeZN3sj6WwSzScFmY3JgWfs33ZEJNcaPzC2TEnfZz", + "Issuer": "http://localhost", + "Audience": "http://localhost", + "DefaultUserRole": "User" + }, + "ConnectionStrings": { + "DefaultConnection": "Server=db;Database=qtcdb;Uid=root;Pwd=EuK3pXkaPCR9cW", + "DevelopmentConnection": "Data Source=qtcdev.db" + }, + "GeneralConfig": { + "CDNPath": "./user-content" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/qtc-net-server/libman.json b/qtc-net-server/libman.json new file mode 100644 index 0000000..ceee271 --- /dev/null +++ b/qtc-net-server/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/qtc-net-server/qtc-net-server.csproj b/qtc-net-server/qtc-net-server.csproj new file mode 100644 index 0000000..578d92e --- /dev/null +++ b/qtc-net-server/qtc-net-server.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + enable + enable + qtc_api + Linux + ..\docker-compose.dcproj + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + Always + + + + diff --git a/qtc-net-server/run.Development.bat b/qtc-net-server/run.Development.bat new file mode 100644 index 0000000..4d11bc6 --- /dev/null +++ b/qtc-net-server/run.Development.bat @@ -0,0 +1,2 @@ +@ECHO OFF +dotnet run --project qtc-net-server.csproj -lp http -e ASPNETCORE_ENVIRONMENT="Development" \ No newline at end of file diff --git a/qtc-net-server/user-content/523736357658921388/523736357658921388.jpg b/qtc-net-server/user-content/523736357658921388/523736357658921388.jpg new file mode 100644 index 0000000..7c03d48 Binary files /dev/null and b/qtc-net-server/user-content/523736357658921388/523736357658921388.jpg differ diff --git a/qtc-net-server/user-content/523736357658921388/523736357658921388.png b/qtc-net-server/user-content/523736357658921388/523736357658921388.png new file mode 100644 index 0000000..a446686 Binary files /dev/null and b/qtc-net-server/user-content/523736357658921388/523736357658921388.png differ diff --git a/qtc-net-server/user-content/600864666738527165/600864666738527165.png b/qtc-net-server/user-content/600864666738527165/600864666738527165.png new file mode 100644 index 0000000..481921b Binary files /dev/null and b/qtc-net-server/user-content/600864666738527165/600864666738527165.png differ diff --git a/qtc-net-server/user-content/740743496563474455/740743496563474455.jpg b/qtc-net-server/user-content/740743496563474455/740743496563474455.jpg new file mode 100644 index 0000000..7c03d48 Binary files /dev/null and b/qtc-net-server/user-content/740743496563474455/740743496563474455.jpg differ