("EmailConfig:SMTPSenderAddress");
+
+ if (!confirmationRequired)
+ {
+ _logger.LogInformation("Email Confirmation Is Disabled. For Better Security Of Your Instance, Consider Setting Up An Email Sender Service.");
+ return;
+ }
+ else if (host == null || username == null || password == null || senderAddress == null)
+ {
+ _logger.LogInformation("Email Confirmation Is Enabled But No SMTP Settings Were Set.");
+ return;
+ }
+
+ // set email subject
+ string emailSubject = "QtC.NET Email Confirmation";
+
+ // build confirmation email body
+ StringBuilder emailBody = new();
+ emailBody.AppendLine($"Hello {name},
");
+ emailBody.AppendLine("Your receiving this message because you requested a password reset on a QtC.NET Account.
");
+ emailBody.AppendLine(@$"You can reset your password by clicking here.
");
+ emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.
");
+ emailBody.AppendLine("If you did not request a password reset on any QtC.NET account on any server, you may simply ignore this email.
");
+
+ // create new client
+ using var client = new SmtpClient(host, port ?? 587)
+ {
+ Credentials = new NetworkCredential(username, password),
+ EnableSsl = true,
+ };
+
+ // send email
+ MailMessage mailMessage = new();
+
+ MailAddress.TryCreate(senderAddress, out MailAddress? from);
+ if (from == null)
+ return;
+
+ MailAddress.TryCreate(email, out MailAddress? to);
+ if (to == null)
+ return;
+
+ mailMessage.IsBodyHtml = true;
+ mailMessage.To.Add(to);
+ mailMessage.From = from;
+ mailMessage.Subject = emailSubject;
+ mailMessage.Body = emailBody.ToString();
+
+ await client.SendMailAsync(mailMessage);
+ }
+ }
+}
diff --git a/qtc-net-server/Services/EmailService/IEmailService.cs b/qtc-net-server/Services/EmailService/IEmailService.cs
new file mode 100644
index 0000000..015da9b
--- /dev/null
+++ b/qtc-net-server/Services/EmailService/IEmailService.cs
@@ -0,0 +1,8 @@
+namespace qtc_api.Services.EmailService
+{
+ public interface IEmailService
+ {
+ public Task SendConfirmationEmail(string email, string name, string confirmUrl);
+ public Task SendPasswordResetEmail(string email, string name, string passwordResetUrl);
+ }
+}
diff --git a/qtc-net-server/Services/GameRoomService/GameRoomService.cs b/qtc-net-server/Services/GameRoomService/GameRoomService.cs
new file mode 100644
index 0000000..5fbe8ac
--- /dev/null
+++ b/qtc-net-server/Services/GameRoomService/GameRoomService.cs
@@ -0,0 +1,10 @@
+namespace qtc_api.Services.GameRoomService
+{
+ public class GameRoomService
+ {
+ public List GameRooms { get; set; } = [];
+
+ public void AddRoom(GameRoom room) => GameRooms.Add(room);
+ public void RemoveRoom(GameRoom room) => GameRooms.Remove(room);
+ }
+}
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..bc41357
--- /dev/null
+++ b/qtc-net-server/Services/RoomService/RoomService.cs
@@ -0,0 +1,110 @@
+namespace qtc_api.Services.RoomService
+{
+ public class RoomService : IRoomService
+ {
+ private readonly DataContext _dataContext;
+
+ private long idMax = 900000000000000000;
+
+ public RoomService(DataContext dataContext)
+ {
+ _dataContext = dataContext;
+ }
+
+ public async Task> 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/StoreService/StoreService.cs b/qtc-net-server/Services/StoreService/StoreService.cs
new file mode 100644
index 0000000..d849482
--- /dev/null
+++ b/qtc-net-server/Services/StoreService/StoreService.cs
@@ -0,0 +1,95 @@
+using System.Text.Json;
+
+namespace qtc_api.Services.StoreService
+{
+ public class StoreService
+ {
+ public static List StoreItems { get; set; } = [];
+
+ private readonly DataContext _ctx;
+ private readonly IUserService _userService;
+ public StoreService(DataContext ctx, IUserService userService)
+ {
+ _ctx = ctx;
+ _userService = userService;
+
+ string storeJsonLocation = "./Resources/store.json";
+
+ if (System.Diagnostics.Debugger.IsAttached)
+ storeJsonLocation = "./Resources/store.Development.json";
+
+ StoreItem[]? storeItems = JsonSerializer.Deserialize(File.ReadAllText(storeJsonLocation));
+ if (storeItems != null && storeItems.Length > 0)
+ {
+ StoreItems = storeItems.ToList();
+ }
+ }
+
+ public ServiceResponse> GetStoreItems()
+ {
+ return new ServiceResponse> { Success = true, Data = StoreItems };
+ }
+
+ public ServiceResponse GetStoreItem(int id)
+ {
+ var item = StoreItems.FirstOrDefault(e => e.Id == id);
+ if (item != null)
+ return new ServiceResponse { Success = true, Data = item };
+ else return new ServiceResponse { Success = false, Message = "Item Not Found" };
+ }
+
+ public ServiceResponse GetBoughtStoreItemFromUser(string userId, int itemId)
+ {
+ // find item owned by user
+ var item = _ctx.OwnedStoreItems.FirstOrDefault(e => e.UserId == userId && e.StoreItemId == itemId);
+ if (item != null)
+ return new ServiceResponse { Success = true, Data = item };
+ else return new ServiceResponse { Success = false, Message = "Item Not Yet Purchased" };
+ }
+
+ public ServiceResponse> GetBoughtStoreItemsFromUser(string userId)
+ {
+ // find items owned by user
+ var items = _ctx.OwnedStoreItems.Where(e => e.UserId == userId).ToList();
+ if (items != null && items.Count > 0)
+ return new ServiceResponse> { Success = true, Data = items };
+ else return new ServiceResponse> { Success = false, Message = "User Owns No Items" };
+ }
+
+ public async Task> BuyStoreItem(string userId, int id)
+ {
+ // find item in store
+ var item = StoreItems.FirstOrDefault(e => e.Id == id);
+ if (item != null)
+ {
+ // deduct currency from user
+ var user = await _userService.GetUserById(userId);
+ if (user != null && user.Success && user.Data != null)
+ {
+ if (user.Data.CurrencyAmount >= item.Price)
+ {
+ // remove currency from user
+ await _userService.RemoveCurrencyFromUser(userId, item.Price);
+
+ // create owned item
+ OwnedStoreItem ownedStoreItem = new OwnedStoreItem
+ {
+ StoreItemId = item.Id,
+ UserId = userId
+ };
+
+ // add to table
+ _ctx.OwnedStoreItems.Add(ownedStoreItem);
+ await _ctx.SaveChangesAsync();
+
+ // return successful service response
+ return new ServiceResponse { Success = true, Data = true };
+ }
+ else return new ServiceResponse { Success = false, Message = "Insufficient Currency" };
+ }
+ else return new ServiceResponse { Success = false, Message = "User Not Found" };
+ }
+ else return new ServiceResponse { Success = false, Message = "Item Not Found" };
+ }
+ }
+}
diff --git a/qtc-net-server/Services/TokenService/ITokenService.cs b/qtc-net-server/Services/TokenService/ITokenService.cs
new file mode 100644
index 0000000..df7b1ed
--- /dev/null
+++ b/qtc-net-server/Services/TokenService/ITokenService.cs
@@ -0,0 +1,12 @@
+namespace qtc_api.Services.TokenService
+{
+ public interface ITokenService
+ {
+ public Task> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false);
+ public ServiceResponse GenerateEmailConfirmationToken(User user);
+ public ServiceResponse GeneratePasswordResetConfirmationToken(User user);
+ 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..4b93d8f
--- /dev/null
+++ b/qtc-net-server/Services/TokenService/TokenService.cs
@@ -0,0 +1,285 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Security.Cryptography;
+
+namespace qtc_api.Services.TokenService
+{
+ public class TokenService : ITokenService
+ {
+ private readonly IConfiguration _configuration;
+ private readonly DataContext _dataContext;
+
+ public TokenService(IConfiguration configuration, DataContext dataContext)
+ {
+ _configuration = configuration;
+ _dataContext = dataContext;
+ }
+
+ public async Task> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken, bool remember)
+ {
+ var serviceResponse = new ServiceResponse();
+
+ // Generate JWT Access Token
+
+ List claims = new List()
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.Id),
+ new Claim(ClaimTypes.Name, user.Username),
+ new Claim(ClaimTypes.Email, user.Email),
+ new Claim(ClaimTypes.Role, user.Role),
+ new Claim("TokenType", "access")
+ };
+
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
+ var issuer = _configuration["Jwt:Issuer"];
+ var audience = _configuration["Jwt:Audience"];
+
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var token = new JwtSecurityToken(
+ issuer: issuer,
+ audience: audience,
+ claims: claims,
+ expires: DateTime.UtcNow.AddHours(1),
+ signingCredentials: creds
+ );
+
+ var jwt = new JwtSecurityTokenHandler().WriteToken(token);
+
+ serviceResponse.Data = jwt;
+
+ // Generate and Store Refresh Token
+
+ if (generateRefToken)
+ {
+ var random = new byte[32];
+ using (var rng = RandomNumberGenerator.Create())
+ {
+ rng.GetBytes(random);
+ }
+
+ RefreshToken refToken = new RefreshToken()
+ {
+ ID = LongRandom(1, 900000000000000000, new Random()).ToString(),
+ UserID = user.Id,
+ Token = Convert.ToBase64String(random)
+ };
+
+ if (remember) refToken.Expires = DateTime.UtcNow.AddMonths(1);
+ else refToken.Expires = DateTime.UtcNow.AddDays(3);
+
+ _dataContext.ValidRefreshTokens.Add(refToken);
+
+ await _dataContext.SaveChangesAsync();
+
+ serviceResponse.Message = refToken.Token;
+ }
+
+ serviceResponse.Success = true;
+ return serviceResponse;
+ }
+
+ public ServiceResponse GenerateEmailConfirmationToken(User user)
+ {
+ var serviceResponse = new ServiceResponse();
+
+ // Generate JWT Access Token
+
+ List claims = new List()
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.Id),
+ new Claim(ClaimTypes.Email, user.Email),
+ new Claim("TokenType", "email-confirmation")
+ };
+
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
+ var issuer = _configuration["Jwt:Issuer"];
+ var audience = _configuration["Jwt:Audience"];
+
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var token = new JwtSecurityToken(
+ issuer: issuer,
+ audience: audience,
+ claims: claims,
+ expires: DateTime.UtcNow.AddHours(24),
+ signingCredentials: creds
+ );
+
+ var jwt = new JwtSecurityTokenHandler().WriteToken(token);
+
+ serviceResponse.Success = true;
+ serviceResponse.Data = jwt;
+
+ return serviceResponse;
+ }
+
+ public ServiceResponse GeneratePasswordResetConfirmationToken(User user)
+ {
+ var serviceResponse = new ServiceResponse