whole bunch of shit

- admin routes
- developer status and bots
- probably a few other tweaks i forgot
This commit is contained in:
Elijah R 2024-04-07 14:43:50 -04:00
parent b627ab383f
commit c68451cf07
21 changed files with 874 additions and 34 deletions

11
CollabVMAuthServer/Bot.cs Normal file
View file

@ -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; }
}

View file

@ -37,7 +37,9 @@ public class Database
password_reset_code CHAR(8) DEFAULT NULL, password_reset_code CHAR(8) DEFAULT NULL,
cvm_rank INT UNSIGNED NOT NULL DEFAULT 1, cvm_rank INT UNSIGNED NOT NULL DEFAULT 1,
banned BOOLEAN NOT NULL DEFAULT 0, 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(); await cmd.ExecuteNonQueryAsync();
@ -62,6 +64,18 @@ public class Database
) )
"""; """;
await cmd.ExecuteNonQueryAsync(); 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<User?> GetUser(string? username = null, string? email = null) public async Task<User?> 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"), PasswordResetCode = reader.IsDBNull("password_reset_code") ? null : reader.GetString("password_reset_code"),
Rank = (Rank)reader.GetUInt32("cvm_rank"), Rank = (Rank)reader.GetUInt32("cvm_rank"),
Banned = reader.GetBoolean("banned"), Banned = reader.GetBoolean("banned"),
RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")) RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")),
Joined = reader.GetDateTime("created"),
Developer = reader.GetBoolean("developer")
}; };
} }
@ -230,7 +246,7 @@ public class Database
await cmd.ExecuteNonQueryAsync(); 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 using var db = new MySqlConnection(connectionString);
await db.OpenAsync(); await db.OpenAsync();
@ -251,6 +267,17 @@ public class Database
updates.Add("email = @newEmail"); updates.Add("email = @newEmail");
cmd.Parameters.AddWithValue("@newEmail", 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.CommandText = $"UPDATE users SET {string.Join(", ", updates)} WHERE username = @username";
cmd.Parameters.AddWithValue("@username", username); cmd.Parameters.AddWithValue("@username", username);
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
@ -307,4 +334,148 @@ public class Database
cmd.Parameters.AddWithValue("@username", username); cmd.Parameters.AddWithValue("@username", username);
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
} }
public async Task<User[]> 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<string>();
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<User>();
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<byte[]>("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<Bot[]> ListBots(string? owner = null)
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
var where = new List<string>();
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<Bot>();
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<string>();
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<Bot?> 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")
};
}
} }

View file

@ -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<IResult> 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<AdminUpdateBotPayload>();
}
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<IResult> 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<AdminUpdateUserPayload>();
}
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<IResult> 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<AdminUsersPayload>();
}
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);
}
}

View file

@ -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<IResult> 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<ListBotsPayload>();
}
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<IResult> 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<CreateBotPayload>();
}
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);
}
}

View file

@ -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; }
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,7 @@
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
public class CreateBotPayload
{
public string token { get; set; }
public string username { get; set; }
}

View file

@ -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; }
}

View file

@ -0,0 +1,7 @@
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
public class AdminUpdateBotResponse
{
public bool success { get; set; }
public string? error { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
public class AdminUpdateUserResponse
{
public bool success { get; set; }
public string? error { get; set; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -8,4 +8,5 @@ public class LoginResponse
public bool? verificationRequired { get; set; } public bool? verificationRequired { get; set; }
public string? email { get; set; } public string? email { get; set; }
public string? username { get; set; } public string? username { get; set; }
public int rank { get; set; }
} }

View file

@ -7,4 +7,5 @@ public class SessionResponse
public bool banned { get; set; } = false; public bool banned { get; set; } = false;
public string? username { get; set; } public string? username { get; set; }
public string? email { get; set; } public string? email { get; set; }
public int rank { get; set; }
} }

View file

@ -269,8 +269,7 @@ public static class Routes
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Make sure username isn't taken // Make sure username isn't taken
var _user = await Program.Database.GetUser(payload.username); if (await Program.Database.GetUser(payload.username) != null || await Program.Database.GetBot(payload.username) != null)
if (_user != null)
{ {
context.Response.StatusCode = 400; context.Response.StatusCode = 400;
return Results.Json(new RegisterResponse return Results.Json(new RegisterResponse
@ -462,7 +461,7 @@ public static class Routes
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Check if session is expired // Check if session is expired
if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays)) if (Utilities.IsSessionExpired(session))
{ {
return Results.Json(new SessionResponse return Results.Json(new SessionResponse
{ {
@ -477,7 +476,8 @@ public static class Routes
success = true, success = true,
banned = user.Banned, banned = user.Banned,
username = user.Username, username = user.Username,
email = user.Email email = user.Email,
rank = (int)user.Rank
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
@ -549,6 +549,9 @@ public static class Routes
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Check if session is valid // Check if session is valid
if (payload.sessionToken.Length == 32)
{
// User
var session = await Program.Database.GetSession(payload.sessionToken); var session = await Program.Database.GetSession(payload.sessionToken);
if (session == null) if (session == null)
{ {
@ -590,6 +593,36 @@ public static class Routes
username = session.Username, username = session.Username,
rank = user.Rank rank = user.Rank
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} 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 = true,
username = bot.Username,
rank = bot.Rank
}, Utilities.JsonSerializerOptions);
}
else
{
context.Response.StatusCode = 400;
return Results.Json(new JoinResponse
{
success = false,
error = "Invalid session"
}, Utilities.JsonSerializerOptions);
}
} }
private static async Task<IResult> HandleLogin(HttpContext context) private static async Task<IResult> HandleLogin(HttpContext context)
@ -689,6 +722,7 @@ public static class Routes
verificationRequired = true, verificationRequired = true,
email = user.Email, email = user.Email,
username = user.Username, username = user.Username,
rank = (int)user.Rank
}); });
} }
// Check max sessions // Check max sessions
@ -706,7 +740,8 @@ public static class Routes
success = true, success = true,
token = token, token = token,
username = user.Username, username = user.Username,
email = user.Email email = user.Email,
rank = (int)user.Rank
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
@ -876,7 +911,7 @@ public static class Routes
} }
// Make sure username isn't taken // Make sure username isn't taken
var user = await Program.Database.GetUser(payload.username); 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; context.Response.StatusCode = 400;
return Results.Json(new RegisterResponse return Results.Json(new RegisterResponse

View file

@ -74,6 +74,8 @@ public class Program
app.Lifetime.ApplicationStarted.Register(() => Utilities.Log(LogLevel.INFO, $"Webserver listening on {Config.HTTP.Host}:{Config.HTTP.Port}")); app.Lifetime.ApplicationStarted.Register(() => Utilities.Log(LogLevel.INFO, $"Webserver listening on {Config.HTTP.Host}:{Config.HTTP.Port}"));
// Register routes // Register routes
Routes.RegisterRoutes(app); Routes.RegisterRoutes(app);
AdminRoutes.RegisterRoutes(app);
DeveloperRoutes.RegisterRoutes(app);
app.Run(); app.Run();
} }
} }

View file

@ -15,6 +15,8 @@ public class User
public Rank Rank { get; set; } public Rank Rank { get; set; }
public bool Banned { get; set; } public bool Banned { get; set; }
public IPAddress RegistrationIP { get; set; } public IPAddress RegistrationIP { get; set; }
public DateTime Joined { get; set; }
public bool Developer { get; set; }
} }
public enum Rank : uint public enum Rank : uint

View file

@ -120,4 +120,9 @@ public static class Utilities
} }
else return ctx.Connection.RemoteIpAddress; else return ctx.Connection.RemoteIpAddress;
} }
public static bool IsSessionExpired(Session session)
{
return DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays);
}
} }