From c68451cf07adb5aa0f9e5f8e0e325e72e701d764 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Sun, 7 Apr 2024 14:43:50 -0400 Subject: [PATCH] whole bunch of shit - admin routes - developer status and bots - probably a few other tweaks i forgot --- CollabVMAuthServer/Bot.cs | 11 + CollabVMAuthServer/Database.cs | 177 +++++++++- CollabVMAuthServer/HTTP/AdminRoutes.cs | 309 ++++++++++++++++++ CollabVMAuthServer/HTTP/DeveloperRoutes.cs | 196 +++++++++++ .../HTTP/Payloads/AdminUpdateBotPayload.cs | 8 + .../HTTP/Payloads/AdminUpdateUserPayload.cs | 9 + .../HTTP/Payloads/AdminUsersPayload.cs | 11 + .../HTTP/Payloads/CreateBotPayload.cs | 7 + .../HTTP/Payloads/ListBotsPayload.cs | 9 + .../HTTP/Responses/AdminUpdateBotResponse.cs | 7 + .../HTTP/Responses/AdminUpdateUserResponse.cs | 7 + .../HTTP/Responses/AdminUsersResponse.cs | 22 ++ .../HTTP/Responses/CreateBotResponse.cs | 8 + CollabVMAuthServer/HTTP/Responses/ListBot.cs | 10 + .../HTTP/Responses/ListBotsResponse.cs | 9 + .../HTTP/Responses/LoginResponse.cs | 1 + .../HTTP/Responses/SessionResponse.cs | 1 + CollabVMAuthServer/HTTP/Routes.cs | 97 ++++-- CollabVMAuthServer/Program.cs | 2 + CollabVMAuthServer/User.cs | 2 + CollabVMAuthServer/Utilities.cs | 5 + 21 files changed, 874 insertions(+), 34 deletions(-) create mode 100644 CollabVMAuthServer/Bot.cs create mode 100644 CollabVMAuthServer/HTTP/AdminRoutes.cs create mode 100644 CollabVMAuthServer/HTTP/DeveloperRoutes.cs create mode 100644 CollabVMAuthServer/HTTP/Payloads/AdminUpdateBotPayload.cs create mode 100644 CollabVMAuthServer/HTTP/Payloads/AdminUpdateUserPayload.cs create mode 100644 CollabVMAuthServer/HTTP/Payloads/AdminUsersPayload.cs create mode 100644 CollabVMAuthServer/HTTP/Payloads/CreateBotPayload.cs create mode 100644 CollabVMAuthServer/HTTP/Payloads/ListBotsPayload.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/AdminUpdateBotResponse.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/AdminUpdateUserResponse.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/AdminUsersResponse.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/CreateBotResponse.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/ListBot.cs create mode 100644 CollabVMAuthServer/HTTP/Responses/ListBotsResponse.cs diff --git a/CollabVMAuthServer/Bot.cs b/CollabVMAuthServer/Bot.cs new file mode 100644 index 0000000..a1cc048 --- /dev/null +++ b/CollabVMAuthServer/Bot.cs @@ -0,0 +1,11 @@ +namespace Computernewb.CollabVMAuthServer; + +public class Bot +{ + public uint Id { get; set; } + public string Username { get; set; } + public string Token { get; set; } + public Rank Rank { get; set; } + public string Owner { get; set; } + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/Database.cs b/CollabVMAuthServer/Database.cs index c353f83..b14d99c 100644 --- a/CollabVMAuthServer/Database.cs +++ b/CollabVMAuthServer/Database.cs @@ -37,7 +37,9 @@ public class Database password_reset_code CHAR(8) DEFAULT NULL, cvm_rank INT UNSIGNED NOT NULL DEFAULT 1, banned BOOLEAN NOT NULL DEFAULT 0, - registration_ip VARBINARY(16) NOT NULL + registration_ip VARBINARY(16) NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + developer BOOLEAN NOT NULL DEFAULT 0 ); """; await cmd.ExecuteNonQueryAsync(); @@ -62,6 +64,18 @@ public class Database ) """; await cmd.ExecuteNonQueryAsync(); + cmd.CommandText = """ + CREATE TABLE IF NOT EXISTS bots ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(20) NOT NULL UNIQUE KEY, + token CHAR(64) NOT NULL UNIQUE KEY, + cvm_rank INT UNSIGNED NOT NULL DEFAULT 1, + owner VARCHAR(20) NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner) REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE + ) + """; + await cmd.ExecuteNonQueryAsync(); } public async Task GetUser(string? username = null, string? email = null) @@ -96,7 +110,9 @@ public class Database PasswordResetCode = reader.IsDBNull("password_reset_code") ? null : reader.GetString("password_reset_code"), Rank = (Rank)reader.GetUInt32("cvm_rank"), Banned = reader.GetBoolean("banned"), - RegistrationIP = new IPAddress(reader.GetFieldValue("registration_ip")) + RegistrationIP = new IPAddress(reader.GetFieldValue("registration_ip")), + Joined = reader.GetDateTime("created"), + Developer = reader.GetBoolean("developer") }; } @@ -230,7 +246,7 @@ public class Database await cmd.ExecuteNonQueryAsync(); } - public async Task UpdateUser(string username, string? newUsername = null, string? newPassword = null, string? newEmail = null) + public async Task UpdateUser(string username, string? newUsername = null, string? newPassword = null, string? newEmail = null, int? newRank = null, bool? developer = null) { await using var db = new MySqlConnection(connectionString); await db.OpenAsync(); @@ -251,6 +267,17 @@ public class Database updates.Add("email = @newEmail"); cmd.Parameters.AddWithValue("@newEmail", newEmail); } + + if (newRank != null) + { + updates.Add("cvm_rank = @newRank"); + cmd.Parameters.AddWithValue("@newRank", newRank); + } + if (developer != null) + { + updates.Add("developer = @developer"); + cmd.Parameters.AddWithValue("@developer", developer); + } cmd.CommandText = $"UPDATE users SET {string.Join(", ", updates)} WHERE username = @username"; cmd.Parameters.AddWithValue("@username", username); await cmd.ExecuteNonQueryAsync(); @@ -307,4 +334,148 @@ public class Database cmd.Parameters.AddWithValue("@username", username); await cmd.ExecuteNonQueryAsync(); } + + public async Task ListUsers(string? filterUsername = null, string orderBy = "id", bool descending = false) + { + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + var where = new List(); + if (filterUsername != null) + { + where.Add("username LIKE @filterUsername"); + cmd.Parameters.AddWithValue("@filterUsername", filterUsername); + } + cmd.CommandText = $"SELECT * FROM users {(where.Count > 0 ? "WHERE" : "")} {string.Join(" AND ", where)} ORDER BY {orderBy} {(descending ? "DESC" : "ASC")}"; + await using var reader = await cmd.ExecuteReaderAsync(); + var users = new List(); + while (await reader.ReadAsync()) + { + users.Add(new User + { + Id = reader.GetUInt32("id"), + Username = reader.GetString("username"), + Password = reader.GetString("password"), + Email = reader.GetString("email"), + DateOfBirth = reader.GetDateOnly("date_of_birth"), + EmailVerified = reader.GetBoolean("email_verified"), + EmailVerificationCode = reader.IsDBNull("email_verification_code") ? null : reader.GetString("email_verification_code"), + PasswordResetCode = reader.IsDBNull("password_reset_code") ? null : reader.GetString("password_reset_code"), + Rank = (Rank)reader.GetUInt32("cvm_rank"), + Banned = reader.GetBoolean("banned"), + RegistrationIP = new IPAddress(reader.GetFieldValue("registration_ip")), + Joined = reader.GetDateTime("created"), + Developer = reader.GetBoolean("developer") + }); + } + return users.ToArray(); + } + + public async Task CreateBot(string username, string token, string owner) + { + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + cmd.CommandText = "INSERT INTO bots (username, token, owner) VALUES (@username, @token, @owner)"; + cmd.Parameters.AddWithValue("@username", username); + cmd.Parameters.AddWithValue("@token", token); + cmd.Parameters.AddWithValue("@owner", owner); + await cmd.ExecuteNonQueryAsync(); + } + + public async Task ListBots(string? owner = null) + { + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + var where = new List(); + if (owner != null) + { + where.Add("owner = @owner"); + cmd.Parameters.AddWithValue("@owner", owner); + } + cmd.CommandText = $"SELECT * FROM bots {(where.Count > 0 ? "WHERE" : "")} {string.Join(" AND ", where)}"; + await using var reader = await cmd.ExecuteReaderAsync(); + var bots = new List(); + while (await reader.ReadAsync()) + { + bots.Add(new Bot + { + Id = reader.GetUInt32("id"), + Username = reader.GetString("username"), + Token = reader.GetString("token"), + Rank = (Rank)reader.GetUInt32("cvm_rank"), + Owner = reader.GetString("owner"), + Created = reader.GetDateTime("created") + }); + } + return bots.ToArray(); + } + + public async Task UpdateBot(string username, string? newUsername = null, string? newToken = null, int? newRank = null) + { + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + var updates = new List(); + if (newUsername != null) + { + updates.Add("username = @username"); + cmd.Parameters.AddWithValue("@username", newUsername); + } + if (newToken != null) + { + updates.Add("token = @token"); + cmd.Parameters.AddWithValue("@token", newToken); + } + if (newRank != null) + { + updates.Add("cvm_rank = @rank"); + cmd.Parameters.AddWithValue("@rank", newRank); + } + cmd.CommandText = $"UPDATE bots SET {string.Join(", ", updates)} WHERE username = @username"; + cmd.Parameters.AddWithValue("@username", username); + await cmd.ExecuteNonQueryAsync(); + } + + public async Task DeleteBots(string owner) + { + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + cmd.CommandText = "DELETE FROM bots WHERE owner = @owner"; + cmd.Parameters.AddWithValue("@owner", owner); + await cmd.ExecuteNonQueryAsync(); + } + + public async Task GetBot(string? username = null, string? token = null) + { + if (username == null && token == null) + throw new ArgumentException("username or token must be provided"); + await using var db = new MySqlConnection(connectionString); + await db.OpenAsync(); + await using var cmd = db.CreateCommand(); + if (username != null) + { + cmd.CommandText = "SELECT * FROM bots WHERE username = @username"; + cmd.Parameters.AddWithValue("@username", username); + } + else if (token != null) + { + cmd.CommandText = "SELECT * FROM bots WHERE token = @token"; + cmd.Parameters.AddWithValue("@token", token); + } + await using var reader = await cmd.ExecuteReaderAsync(); + if (!await reader.ReadAsync()) + return null; + return new Bot + { + Id = reader.GetUInt32("id"), + Username = reader.GetString("username"), + Token = reader.GetString("token"), + Rank = (Rank)reader.GetUInt32("cvm_rank"), + Owner = reader.GetString("owner"), + Created = reader.GetDateTime("created") + }; + } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/AdminRoutes.cs b/CollabVMAuthServer/HTTP/AdminRoutes.cs new file mode 100644 index 0000000..021669d --- /dev/null +++ b/CollabVMAuthServer/HTTP/AdminRoutes.cs @@ -0,0 +1,309 @@ +using System.Text.Json; +using Computernewb.CollabVMAuthServer.HTTP.Payloads; +using Computernewb.CollabVMAuthServer.HTTP.Responses; + +namespace Computernewb.CollabVMAuthServer.HTTP; + +public static class AdminRoutes +{ + public static void RegisterRoutes(IEndpointRouteBuilder app) + { + app.MapPost("/api/v1/admin/users", (Delegate)HandleAdminUsers); + app.MapPost("/api/v1/admin/updateuser", (Delegate)HandleAdminUpdateUser); + app.MapPost("/api/v1/admin/updatebot", (Delegate)HandleAdminUpdateBot); + } + + private static async Task HandleAdminUpdateBot(HttpContext context) + { + // Check payload + if (context.Request.ContentType != "application/json") + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + AdminUpdateBotPayload? payload; + try + { + payload = await context.Request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}"); + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + // Check token + var session = await Program.Database.GetSession(payload.token); + if (session == null || Utilities.IsSessionExpired(session)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Invalid session" + }, Utilities.JsonSerializerOptions); + } + // Check rank + var user = await Program.Database.GetUser(session.Username) + ?? throw new Exception("Could not lookup user from session"); + if (user.Rank != Rank.Admin) + { + context.Response.StatusCode = 403; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Insufficient permissions" + }, Utilities.JsonSerializerOptions); + } + // Check target bot + var targetBot = await Program.Database.GetBot(payload.username); + if (targetBot == null) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Bot not found" + }, Utilities.JsonSerializerOptions); + } + // Make sure at least one field is being updated + if (payload.rank == null) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "No fields to update" + }, Utilities.JsonSerializerOptions); + } + // Check rank + int? rank = payload.rank; + if (rank != null && rank < 1 || rank > 3) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateBotResponse + { + success = false, + error = "Invalid rank" + }, Utilities.JsonSerializerOptions); + } + // Update rank + await Program.Database.UpdateBot(targetBot.Username, newRank: payload.rank); + return Results.Json(new AdminUpdateBotResponse + { + success = true + }, Utilities.JsonSerializerOptions); + } + + private static async Task HandleAdminUpdateUser(HttpContext context) + { + // Check payload + if (context.Request.ContentType != "application/json") + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + AdminUpdateUserPayload? payload; + try + { + payload = await context.Request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}"); + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + // Check token + var session = await Program.Database.GetSession(payload.token); + if (session == null || Utilities.IsSessionExpired(session)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "Invalid session" + }, Utilities.JsonSerializerOptions); + } + // Check rank + var user = await Program.Database.GetUser(session.Username) + ?? throw new Exception("Could not lookup user from session"); + if (user.Rank != Rank.Admin) + { + context.Response.StatusCode = 403; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Insufficient permissions" + }, Utilities.JsonSerializerOptions); + } + // Check target user + var targetUser = await Program.Database.GetUser(payload.username); + if (targetUser == null) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "User not found" + }, Utilities.JsonSerializerOptions); + } + // Check rank + int? rank = payload.rank; + if (rank != null && rank < 1 || rank > 3) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUpdateUserResponse + { + success = false, + error = "Invalid rank" + }, Utilities.JsonSerializerOptions); + } + // Check developer + bool? developer = payload.developer; + // Update rank + await Program.Database.UpdateUser(targetUser.Username, newRank: payload.rank, developer: developer); + if (developer == false) + { + await Program.Database.DeleteBots(targetUser.Username); + } + return Results.Json(new AdminUpdateUserResponse + { + success = true + }, Utilities.JsonSerializerOptions); + } + + private static async Task HandleAdminUsers(HttpContext context) + { + // Check payload + if (context.Request.ContentType != "application/json") + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + AdminUsersPayload? payload; + try + { + payload = await context.Request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}"); + context.Response.StatusCode = 400; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + if (payload == null || string.IsNullOrWhiteSpace(payload.token) || payload.page < 1 || payload.resultsPerPage < 1) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + // Check token + var session = await Program.Database.GetSession(payload.token); + if (session == null || Utilities.IsSessionExpired(session)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Invalid session" + }, Utilities.JsonSerializerOptions); + } + // Check rank + var user = await Program.Database.GetUser(session.Username) + ?? throw new Exception("Could not lookup user from session"); + if (user.Rank != Rank.Admin) + { + context.Response.StatusCode = 403; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Insufficient permissions" + }, Utilities.JsonSerializerOptions); + } + // Validate orderBy + if (payload.orderBy != null && !new string[] { "id", "username", "email", "date_of_birth", "cvm_rank", "banned", "created" }.Contains(payload.orderBy)) + { + context.Response.StatusCode = 400; + return Results.Json(new AdminUsersResponse + { + success = false, + error = "Invalid orderBy" + }, Utilities.JsonSerializerOptions); + } + // Get users + string? filterUsername = null; + if (payload.filterUsername != null) + { + filterUsername = "%" + payload.filterUsername + .Replace("%", "!%") + .Replace("!", "!!") + .Replace("_", "!_") + .Replace("[", "![") + "%"; + } + var users = (await Program.Database.ListUsers(filterUsername, payload.orderBy ?? "id", payload.orderByDescending)).Select(user => new AdminUser + { + id = user.Id, + username = user.Username, + email = user.Email, + rank = (int)user.Rank, + banned = user.Banned, + dateOfBirth = user.DateOfBirth.ToString("yyyy-MM-dd"), + dateJoined = user.Joined.ToString("yyyy-MM-dd HH:mm:ss"), + registrationIp = user.RegistrationIP.ToString(), + developer = user.Developer + }).ToArray(); + var page = users.Skip((payload.page - 1) * payload.resultsPerPage).Take(payload.resultsPerPage).ToArray(); + return Results.Json(new AdminUsersResponse + { + success = true, + users = page, + totalPageCount = (int)Math.Ceiling(users.Length / (double)payload.resultsPerPage) + }, Utilities.JsonSerializerOptions); + } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/DeveloperRoutes.cs b/CollabVMAuthServer/HTTP/DeveloperRoutes.cs new file mode 100644 index 0000000..80406b0 --- /dev/null +++ b/CollabVMAuthServer/HTTP/DeveloperRoutes.cs @@ -0,0 +1,196 @@ +using System.Text.Json; +using Computernewb.CollabVMAuthServer.HTTP.Payloads; +using Computernewb.CollabVMAuthServer.HTTP.Responses; + +namespace Computernewb.CollabVMAuthServer.HTTP; + +public static class DeveloperRoutes +{ + public static void RegisterRoutes(IEndpointRouteBuilder app) + { + app.MapPost("/api/v1/bots/create", (Delegate)HandleCreateBot); + app.MapPost("/api/v1/bots/list", (Delegate)HandleListBots); + } + + private static async Task HandleListBots(HttpContext context) + { + // Check payload + if (context.Request.ContentType != "application/json") + { + context.Response.StatusCode = 400; + return Results.Json(new ListBotsResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + ListBotsPayload? payload; + try + { + payload = await context.Request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}"); + context.Response.StatusCode = 400; + return Results.Json(new ListBotsResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + if (payload == null || string.IsNullOrWhiteSpace(payload.token) || payload.resultsPerPage <= 0 || + payload.page <= 0) + { + context.Response.StatusCode = 400; + return Results.Json(new ListBotsResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + // Check token + var session = await Program.Database.GetSession(payload.token); + if (session == null || Utilities.IsSessionExpired(session)) + { + context.Response.StatusCode = 400; + return Results.Json(new ListBotsResponse + { + success = false, + error = "Invalid session" + }, Utilities.JsonSerializerOptions); + } + // Check developer status + var user = await Program.Database.GetUser(session.Username) ?? + throw new Exception("Unable to get user from session"); + if (!user.Developer && user.Rank != Rank.Admin) + { + context.Response.StatusCode = 403; + return Results.Json(new CreateBotResponse + { + success = false, + error = "You must be an approved developer to create and manage bots." + }, Utilities.JsonSerializerOptions); + } + // owner can only be specified by admins + if (payload.owner != null && user.Rank != Rank.Admin) + { + context.Response.StatusCode = 403; + return Results.Json(new ListBotsResponse + { + success = false, + error = "Insufficient permissions" + }, Utilities.JsonSerializerOptions); + } + // Get bots + // If the user is not an admin, they can only see their own bots + var bots = (await Program.Database.ListBots(payload.owner ?? (user.Rank == Rank.Admin ? null : user.Username))).Select(bot => new ListBot + { + id = (int)bot.Id, + username = bot.Username, + rank = (int)bot.Rank, + owner = bot.Owner, + created = bot.Created.ToString("yyyy-MM-dd HH:mm:ss") + + }); + var page = bots.Skip((payload.page - 1) * payload.resultsPerPage).Take(payload.resultsPerPage).ToArray(); + return Results.Json(new ListBotsResponse + { + success = true, + totalPageCount = (int)Math.Ceiling(bots.Count() / (double)payload.resultsPerPage), + bots = page + }, Utilities.JsonSerializerOptions); + } + + private static async Task HandleCreateBot(HttpContext context) + { + // Check payload + if (context.Request.ContentType != "application/json") + { + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + CreateBotPayload? payload; + try + { + payload = await context.Request.ReadFromJsonAsync(); + } + catch (JsonException ex) + { + Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}"); + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username)) + { + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = "Invalid request body" + }, Utilities.JsonSerializerOptions); + } + // Check token + var session = await Program.Database.GetSession(payload.token); + if (session == null || Utilities.IsSessionExpired(session)) + { + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = "Invalid session" + }, Utilities.JsonSerializerOptions); + } + // Check developer status + var user = await Program.Database.GetUser(session.Username) ?? + throw new Exception("Unable to get user from session"); + if (!user.Developer) + { + context.Response.StatusCode = 403; + return Results.Json(new CreateBotResponse + { + success = false, + error = "You must be an approved developer to create and manage bots." + }, Utilities.JsonSerializerOptions); + } + // Check bot username + if (await Program.Database.GetBot(payload.username) != null || + await Program.Database.GetUser(payload.username) != null) + { + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = "That username is taken." + }, Utilities.JsonSerializerOptions); + } + + if (!Utilities.ValidateUsername(payload.username)) + { + context.Response.StatusCode = 400; + return Results.Json(new CreateBotResponse + { + success = false, + error = + "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and must be between 3 and 20 characters." + }, Utilities.JsonSerializerOptions); + } + // Generate token + string token = Utilities.RandomString(64); + // Create bot + await Program.Database.CreateBot(payload.username, token, user.Username); + return Results.Json(new CreateBotResponse + { + success = true, + token = token + }, Utilities.JsonSerializerOptions); + } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/AdminUpdateBotPayload.cs b/CollabVMAuthServer/HTTP/Payloads/AdminUpdateBotPayload.cs new file mode 100644 index 0000000..497b353 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Payloads/AdminUpdateBotPayload.cs @@ -0,0 +1,8 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Payloads; + +public class AdminUpdateBotPayload +{ + public string token { get; set; } + public string username { get; set; } + public int? rank { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/AdminUpdateUserPayload.cs b/CollabVMAuthServer/HTTP/Payloads/AdminUpdateUserPayload.cs new file mode 100644 index 0000000..1b6cfc1 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Payloads/AdminUpdateUserPayload.cs @@ -0,0 +1,9 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Payloads; + +public class AdminUpdateUserPayload +{ + public string token { get; set; } + public string username { get; set; } + public int? rank { get; set; } + public bool? developer { get; set; } = null; +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/AdminUsersPayload.cs b/CollabVMAuthServer/HTTP/Payloads/AdminUsersPayload.cs new file mode 100644 index 0000000..a5ee163 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Payloads/AdminUsersPayload.cs @@ -0,0 +1,11 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Payloads; + +public class AdminUsersPayload +{ + public string token { get; set; } + public int resultsPerPage { get; set; } + public int page { get; set; } + public string? filterUsername { get; set; } + public string? orderBy { get; set; } + public bool orderByDescending { get; set; } = false; +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/CreateBotPayload.cs b/CollabVMAuthServer/HTTP/Payloads/CreateBotPayload.cs new file mode 100644 index 0000000..85b75f7 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Payloads/CreateBotPayload.cs @@ -0,0 +1,7 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Payloads; + +public class CreateBotPayload +{ + public string token { get; set; } + public string username { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Payloads/ListBotsPayload.cs b/CollabVMAuthServer/HTTP/Payloads/ListBotsPayload.cs new file mode 100644 index 0000000..3d0d1aa --- /dev/null +++ b/CollabVMAuthServer/HTTP/Payloads/ListBotsPayload.cs @@ -0,0 +1,9 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Payloads; + +public class ListBotsPayload +{ + public string token { get; set; } + public int resultsPerPage { get; set; } + public int page { get; set; } + public string? owner { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/AdminUpdateBotResponse.cs b/CollabVMAuthServer/HTTP/Responses/AdminUpdateBotResponse.cs new file mode 100644 index 0000000..6713cd1 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/AdminUpdateBotResponse.cs @@ -0,0 +1,7 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class AdminUpdateBotResponse +{ + public bool success { get; set; } + public string? error { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/AdminUpdateUserResponse.cs b/CollabVMAuthServer/HTTP/Responses/AdminUpdateUserResponse.cs new file mode 100644 index 0000000..3c7a89c --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/AdminUpdateUserResponse.cs @@ -0,0 +1,7 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class AdminUpdateUserResponse +{ + public bool success { get; set; } + public string? error { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/AdminUsersResponse.cs b/CollabVMAuthServer/HTTP/Responses/AdminUsersResponse.cs new file mode 100644 index 0000000..adad16e --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/AdminUsersResponse.cs @@ -0,0 +1,22 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class AdminUsersResponse +{ + public bool success { get; set; } + public string? error { get; set; } + public int? totalPageCount { get; set; } = null; + public AdminUser[]? users { get; set; } +} + +public class AdminUser +{ + public uint id { get; set; } + public string username { get; set; } + public string email { get; set; } + public int rank { get; set; } + public bool banned { get; set; } + public string dateOfBirth { get; set; } + public string dateJoined { get; set; } + public string registrationIp { get; set; } + public bool developer { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/CreateBotResponse.cs b/CollabVMAuthServer/HTTP/Responses/CreateBotResponse.cs new file mode 100644 index 0000000..fe27dc3 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/CreateBotResponse.cs @@ -0,0 +1,8 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class CreateBotResponse +{ + public bool success { get; set; } + public string? error { get; set; } + public string? token { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/ListBot.cs b/CollabVMAuthServer/HTTP/Responses/ListBot.cs new file mode 100644 index 0000000..1a7e049 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/ListBot.cs @@ -0,0 +1,10 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class ListBot +{ + public int id { get; set; } + public string username { get; set; } + public int rank { get; set; } + public string owner { get; set; } + public string created { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/ListBotsResponse.cs b/CollabVMAuthServer/HTTP/Responses/ListBotsResponse.cs new file mode 100644 index 0000000..49aeb35 --- /dev/null +++ b/CollabVMAuthServer/HTTP/Responses/ListBotsResponse.cs @@ -0,0 +1,9 @@ +namespace Computernewb.CollabVMAuthServer.HTTP.Responses; + +public class ListBotsResponse +{ + public bool success { get; set; } + public string? error { get; set; } + public int? totalPageCount { get; set; } = null; + public ListBot[]? bots { get; set; } +} \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/LoginResponse.cs b/CollabVMAuthServer/HTTP/Responses/LoginResponse.cs index 1cb4f39..243d0a4 100644 --- a/CollabVMAuthServer/HTTP/Responses/LoginResponse.cs +++ b/CollabVMAuthServer/HTTP/Responses/LoginResponse.cs @@ -8,4 +8,5 @@ public class LoginResponse public bool? verificationRequired { get; set; } public string? email { get; set; } public string? username { get; set; } + public int rank { get; set; } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Responses/SessionResponse.cs b/CollabVMAuthServer/HTTP/Responses/SessionResponse.cs index 194b3f5..c1af6cf 100644 --- a/CollabVMAuthServer/HTTP/Responses/SessionResponse.cs +++ b/CollabVMAuthServer/HTTP/Responses/SessionResponse.cs @@ -7,4 +7,5 @@ public class SessionResponse public bool banned { get; set; } = false; public string? username { get; set; } public string? email { get; set; } + public int rank { get; set; } } \ No newline at end of file diff --git a/CollabVMAuthServer/HTTP/Routes.cs b/CollabVMAuthServer/HTTP/Routes.cs index 10df638..aba13c7 100644 --- a/CollabVMAuthServer/HTTP/Routes.cs +++ b/CollabVMAuthServer/HTTP/Routes.cs @@ -269,8 +269,7 @@ public static class Routes }, Utilities.JsonSerializerOptions); } // Make sure username isn't taken - var _user = await Program.Database.GetUser(payload.username); - if (_user != null) + if (await Program.Database.GetUser(payload.username) != null || await Program.Database.GetBot(payload.username) != null) { context.Response.StatusCode = 400; return Results.Json(new RegisterResponse @@ -462,7 +461,7 @@ public static class Routes }, Utilities.JsonSerializerOptions); } // Check if session is expired - if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays)) + if (Utilities.IsSessionExpired(session)) { return Results.Json(new SessionResponse { @@ -477,7 +476,8 @@ public static class Routes success = true, banned = user.Banned, username = user.Username, - email = user.Email + email = user.Email, + rank = (int)user.Rank }, Utilities.JsonSerializerOptions); } @@ -549,47 +549,80 @@ public static class Routes }, Utilities.JsonSerializerOptions); } // Check if session is valid - var session = await Program.Database.GetSession(payload.sessionToken); - if (session == null) + if (payload.sessionToken.Length == 32) { + // User + var session = await Program.Database.GetSession(payload.sessionToken); + if (session == null) + { + return Results.Json(new JoinResponse + { + success = true, + clientSuccess = false, + error = "Invalid session", + }, Utilities.JsonSerializerOptions); + } + // Check if session is expired + if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays)) + { + return Results.Json(new JoinResponse + { + success = true, + clientSuccess = false, + error = "Invalid session", + }, Utilities.JsonSerializerOptions); + } + // Check if banned + var user = await Program.Database.GetUser(session.Username) + ?? throw new Exception("User not found in database (something is very wrong)"); + if (user.Banned) + { + return Results.Json(new JoinResponse + { + success = true, + clientSuccess = false, + error = "You are banned", + }, Utilities.JsonSerializerOptions); + } + // Update session + await Program.Database.UpdateSessionLastUsed(session.Token, IPAddress.Parse(payload.ip)); return Results.Json(new JoinResponse { success = true, - clientSuccess = false, - error = "Invalid session", + clientSuccess = true, + username = session.Username, + rank = user.Rank }, Utilities.JsonSerializerOptions); - } - // Check if session is expired - if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays)) + } else if (payload.sessionToken.Length == 64) { + // Bot + var bot = await Program.Database.GetBot(token: payload.sessionToken); + if (bot == null) + { + return Results.Json(new JoinResponse + { + success = true, + clientSuccess = false, + error = "Invalid session", + }, Utilities.JsonSerializerOptions); + } return Results.Json(new JoinResponse { success = true, - clientSuccess = false, - error = "Invalid session", + clientSuccess = true, + username = bot.Username, + rank = bot.Rank }, Utilities.JsonSerializerOptions); } - // Check if banned - var user = await Program.Database.GetUser(session.Username) - ?? throw new Exception("User not found in database (something is very wrong)"); - if (user.Banned) + else { + context.Response.StatusCode = 400; return Results.Json(new JoinResponse { - success = true, - clientSuccess = false, - error = "You are banned", + success = false, + error = "Invalid session" }, Utilities.JsonSerializerOptions); } - // Update session - await Program.Database.UpdateSessionLastUsed(session.Token, IPAddress.Parse(payload.ip)); - return Results.Json(new JoinResponse - { - success = true, - clientSuccess = true, - username = session.Username, - rank = user.Rank - }, Utilities.JsonSerializerOptions); } private static async Task HandleLogin(HttpContext context) @@ -689,6 +722,7 @@ public static class Routes verificationRequired = true, email = user.Email, username = user.Username, + rank = (int)user.Rank }); } // Check max sessions @@ -706,7 +740,8 @@ public static class Routes success = true, token = token, username = user.Username, - email = user.Email + email = user.Email, + rank = (int)user.Rank }, Utilities.JsonSerializerOptions); } @@ -876,7 +911,7 @@ public static class Routes } // Make sure username isn't taken var user = await Program.Database.GetUser(payload.username); - if (user != null) + if (user != null || await Program.Database.GetBot(payload.username) != null) { context.Response.StatusCode = 400; return Results.Json(new RegisterResponse diff --git a/CollabVMAuthServer/Program.cs b/CollabVMAuthServer/Program.cs index db7b206..40600bf 100644 --- a/CollabVMAuthServer/Program.cs +++ b/CollabVMAuthServer/Program.cs @@ -74,6 +74,8 @@ public class Program app.Lifetime.ApplicationStarted.Register(() => Utilities.Log(LogLevel.INFO, $"Webserver listening on {Config.HTTP.Host}:{Config.HTTP.Port}")); // Register routes Routes.RegisterRoutes(app); + AdminRoutes.RegisterRoutes(app); + DeveloperRoutes.RegisterRoutes(app); app.Run(); } } \ No newline at end of file diff --git a/CollabVMAuthServer/User.cs b/CollabVMAuthServer/User.cs index 24943ad..7fb2901 100644 --- a/CollabVMAuthServer/User.cs +++ b/CollabVMAuthServer/User.cs @@ -15,6 +15,8 @@ public class User public Rank Rank { get; set; } public bool Banned { get; set; } public IPAddress RegistrationIP { get; set; } + public DateTime Joined { get; set; } + public bool Developer { get; set; } } public enum Rank : uint diff --git a/CollabVMAuthServer/Utilities.cs b/CollabVMAuthServer/Utilities.cs index 2cb8913..10f3f92 100644 --- a/CollabVMAuthServer/Utilities.cs +++ b/CollabVMAuthServer/Utilities.cs @@ -120,4 +120,9 @@ public static class Utilities } else return ctx.Connection.RemoteIpAddress; } + + public static bool IsSessionExpired(Session session) + { + return DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays); + } } \ No newline at end of file