- add mechanism for database upgrades

- add ban reason
- add api endpoints for banning
- moderators can now list users/bots, and perform basic updates
This commit is contained in:
Elijah R 2024-04-29 12:05:48 -04:00
parent 7e7d9f6e92
commit 130baa8863
15 changed files with 365 additions and 27 deletions

View file

@ -8,6 +8,7 @@
<PublishAot>false</PublishAot> <PublishAot>false</PublishAot>
<RootNamespace>Computernewb.CollabVMAuthServer</RootNamespace> <RootNamespace>Computernewb.CollabVMAuthServer</RootNamespace>
<Company>Computernewb Development Team</Company> <Company>Computernewb Development Team</Company>
<AssemblyVersion>1.1</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -37,6 +37,7 @@ 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,
ban_reason TEXT DEFAULT NULL,
registration_ip VARBINARY(16) NOT NULL, registration_ip VARBINARY(16) NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
developer BOOLEAN NOT NULL DEFAULT 0 developer BOOLEAN NOT NULL DEFAULT 0
@ -76,6 +77,13 @@ public class Database
) )
"""; """;
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS meta (
setting VARCHAR(20) NOT NULL PRIMARY KEY,
val TEXT NOT NULL
)
""";
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)
@ -110,6 +118,7 @@ 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"),
BanReason = reader.IsDBNull("ban_reason") ? null : reader.GetString("ban_reason"),
RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")), RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")),
Joined = reader.GetDateTime("created"), Joined = reader.GetDateTime("created"),
Developer = reader.GetBoolean("developer") Developer = reader.GetBoolean("developer")
@ -363,6 +372,7 @@ 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"),
BanReason = reader.IsDBNull("ban_reason") ? null : reader.GetString("ban_reason"),
RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")), RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")),
Joined = reader.GetDateTime("created"), Joined = reader.GetDateTime("created"),
Developer = reader.GetBoolean("developer") Developer = reader.GetBoolean("developer")
@ -478,4 +488,63 @@ public class Database
Created = reader.GetDateTime("created") Created = reader.GetDateTime("created")
}; };
} }
public async Task<int> GetDatabaseVersion()
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
// If `users` table doesn't exist, return -1. This is hacky but I don't know of a better way
cmd.CommandText = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'users'";
if ((long)(await cmd.ExecuteScalarAsync() ?? 0) == 0)
return -1;
// If `meta` table doesn't exist, assume version 0
cmd.CommandText = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'meta'";
if ((long)(await cmd.ExecuteScalarAsync() ?? 0) == 0)
return 0;
cmd.CommandText = "SELECT val FROM meta WHERE setting = 'db_version'";
await using var reader = await cmd.ExecuteReaderAsync();
await reader.ReadAsync();
return int.Parse(reader.GetString("val"));
}
public async Task SetDatabaseVersion(int version)
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
cmd.CommandText = "INSERT INTO meta (setting, val) VALUES ('db_version', @version) ON DUPLICATE KEY UPDATE val = @version";
cmd.Parameters.AddWithValue("@version", version.ToString());
await cmd.ExecuteNonQueryAsync();
}
public async Task ExecuteNonQuery(string query)
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
cmd.CommandText = query;
await cmd.ExecuteNonQueryAsync();
}
public async Task SetBanned(string username, bool banned, string? reason)
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
cmd.CommandText = "UPDATE users SET banned = @banned, ban_reason = @reason WHERE username = @username";
cmd.Parameters.AddWithValue("@banned", banned);
cmd.Parameters.AddWithValue("@reason", reason);
cmd.Parameters.AddWithValue("@username", username);
await cmd.ExecuteNonQueryAsync();
}
public async Task<long> CountUsers()
{
await using var db = new MySqlConnection(connectionString);
await db.OpenAsync();
await using var cmd = db.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM users";
return (long)await cmd.ExecuteScalarAsync();
}
} }

View file

@ -0,0 +1,33 @@
using System.Collections.ObjectModel;
namespace Computernewb.CollabVMAuthServer;
public static class DatabaseUpdate
{
public const int CurrentVersion = 1;
private static ReadOnlyDictionary<int, Func<Database, Task>> Updates = new Dictionary<int, Func<Database, Task>>()
{
{ 1, async db =>
{
// Update to version 1
// Add ban_reason column to users table
await db.ExecuteNonQuery("ALTER TABLE users ADD COLUMN ban_reason TEXT DEFAULT NULL");
}},
}.AsReadOnly();
public async static Task Update(Database db)
{
var version = await db.GetDatabaseVersion();
if (version == -1) throw new InvalidOperationException("Uninitialized database cannot be updated");
if (version == CurrentVersion) return;
if (version > CurrentVersion) throw new InvalidOperationException("Database version is newer than the server supports");
Utilities.Log(LogLevel.INFO, $"Updating database from version {version} to {CurrentVersion}");
for (int i = version + 1; i <= CurrentVersion; i++)
{
if (!Updates.TryGetValue(i, out var update)) throw new InvalidOperationException($"No update available for version {i}");
await update(db);
}
await db.SetDatabaseVersion(CurrentVersion);
}
}

View file

@ -1,3 +1,4 @@
using System.Net;
using System.Text.Json; using System.Text.Json;
using Computernewb.CollabVMAuthServer.HTTP.Payloads; using Computernewb.CollabVMAuthServer.HTTP.Payloads;
using Computernewb.CollabVMAuthServer.HTTP.Responses; using Computernewb.CollabVMAuthServer.HTTP.Responses;
@ -11,6 +12,160 @@ public static class AdminRoutes
app.MapPost("/api/v1/admin/users", (Delegate)HandleAdminUsers); app.MapPost("/api/v1/admin/users", (Delegate)HandleAdminUsers);
app.MapPost("/api/v1/admin/updateuser", (Delegate)HandleAdminUpdateUser); app.MapPost("/api/v1/admin/updateuser", (Delegate)HandleAdminUpdateUser);
app.MapPost("/api/v1/admin/updatebot", (Delegate)HandleAdminUpdateBot); app.MapPost("/api/v1/admin/updatebot", (Delegate)HandleAdminUpdateBot);
app.MapPost("/api/v1/admin/ban", (Delegate)HandleBanUser);
app.MapPost("/api/v1/admin/ipban", (Delegate)HandleIPBan);
}
private static async Task<IResult> HandleIPBan(HttpContext context)
{
// Check payload
if (context.Request.ContentType != "application/json")
{
context.Response.StatusCode = 400;
return Results.Json(new IPBanResponse
{
success = false,
error = "Invalid request body"
}, Utilities.JsonSerializerOptions);
}
IPBanPayload? payload;
try
{
payload = await context.Request.ReadFromJsonAsync<IPBanPayload>();
}
catch (JsonException ex)
{
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
context.Response.StatusCode = 400;
return Results.Json(new IPBanResponse
{
success = false,
error = "Invalid request body"
}, Utilities.JsonSerializerOptions);
}
if (payload == null || string.IsNullOrWhiteSpace(payload.session) || string.IsNullOrWhiteSpace(payload.ip) || (payload.banned && string.IsNullOrWhiteSpace(payload.reason)) || payload.banned == null || !IPAddress.TryParse(payload.ip, out var ip))
{
context.Response.StatusCode = 400;
return Results.Json(new IPBanResponse
{
success = false,
error = "Invalid request body"
}, Utilities.JsonSerializerOptions);
}
// Check token
var session = await Program.Database.GetSession(payload.session);
if (session == null || Utilities.IsSessionExpired(session))
{
context.Response.StatusCode = 400;
return Results.Json(new IPBanResponse
{
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 && user.Rank != Rank.Moderator)
{
context.Response.StatusCode = 403;
return Results.Json(new IPBanResponse
{
success = false,
error = "Insufficient permissions"
}, Utilities.JsonSerializerOptions);
}
// Set ban
if (payload.banned)
{
await Program.Database.BanIP(ip, payload.reason, user.Username);
}
else
{
await Program.Database.UnbanIP(ip);
}
return Results.Json(new IPBanResponse
{
success = true
}, Utilities.JsonSerializerOptions);
}
private static async Task<IResult> HandleBanUser(HttpContext context)
{
// Check payload
if (context.Request.ContentType != "application/json")
{
context.Response.StatusCode = 400;
return Results.Json(new BanUserResponse
{
success = false,
error = "Invalid request body"
}, Utilities.JsonSerializerOptions);
}
BanUserPayload? payload;
try
{
payload = await context.Request.ReadFromJsonAsync<BanUserPayload>();
}
catch (JsonException ex)
{
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
context.Response.StatusCode = 400;
return Results.Json(new BanUserResponse
{
success = false,
error = "Invalid request body"
}, Utilities.JsonSerializerOptions);
}
if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username) || (payload.banned && string.IsNullOrWhiteSpace(payload.reason)) || payload.banned == null)
{
context.Response.StatusCode = 400;
return Results.Json(new BanUserResponse
{
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 BanUserResponse
{
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 && user.Rank != Rank.Moderator)
{
context.Response.StatusCode = 403;
return Results.Json(new BanUserResponse
{
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 BanUserResponse
{
success = false,
error = "User not found"
}, Utilities.JsonSerializerOptions);
}
// Set ban
await Program.Database.SetBanned(targetUser.Username, payload.banned, payload.banned ? payload.reason : null);
return Results.Json(new BanUserResponse
{
success = true
}, Utilities.JsonSerializerOptions);
} }
private static async Task<IResult> HandleAdminUpdateBot(HttpContext context) private static async Task<IResult> HandleAdminUpdateBot(HttpContext context)
@ -63,7 +218,7 @@ public static class AdminRoutes
// Check rank // Check rank
var user = await Program.Database.GetUser(session.Username) var user = await Program.Database.GetUser(session.Username)
?? throw new Exception("Could not lookup user from session"); ?? throw new Exception("Could not lookup user from session");
if (user.Rank != Rank.Admin) if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
{ {
context.Response.StatusCode = 403; context.Response.StatusCode = 403;
return Results.Json(new AdminUsersResponse return Results.Json(new AdminUsersResponse
@ -93,6 +248,25 @@ public static class AdminRoutes
error = "No fields to update" error = "No fields to update"
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Moderators cannot promote bots to admin, and can only promote their own bots to moderator
else if ((Rank)payload.rank == Rank.Admin && user.Rank == Rank.Moderator)
{
context.Response.StatusCode = 403;
return Results.Json(new AdminUpdateBotResponse
{
success = false,
error = "Insufficient permissions"
}, Utilities.JsonSerializerOptions);
}
if (targetBot.Owner != user.Username && user.Rank == Rank.Moderator)
{
context.Response.StatusCode = 403;
return Results.Json(new AdminUpdateBotResponse
{
success = false,
error = "Insufficient permissions"
}, Utilities.JsonSerializerOptions);
}
// Check rank // Check rank
int? rank = payload.rank; int? rank = payload.rank;
if (rank != null && rank < 1 || rank > 3) if (rank != null && rank < 1 || rank > 3)
@ -193,6 +367,16 @@ public static class AdminRoutes
error = "Invalid rank" error = "Invalid rank"
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Moderators cannot change ranks
if (user.Rank == Rank.Moderator && rank != null)
{
context.Response.StatusCode = 403;
return Results.Json(new AdminUpdateUserResponse
{
success = false,
error = "Insufficient permissions"
}, Utilities.JsonSerializerOptions);
}
// Check developer // Check developer
bool? developer = payload.developer; bool? developer = payload.developer;
// Update rank // Update rank
@ -257,7 +441,7 @@ public static class AdminRoutes
// Check rank // Check rank
var user = await Program.Database.GetUser(session.Username) var user = await Program.Database.GetUser(session.Username)
?? throw new Exception("Could not lookup user from session"); ?? throw new Exception("Could not lookup user from session");
if (user.Rank != Rank.Admin) if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
{ {
context.Response.StatusCode = 403; context.Response.StatusCode = 403;
return Results.Json(new AdminUsersResponse return Results.Json(new AdminUsersResponse
@ -293,6 +477,7 @@ public static class AdminRoutes
email = user.Email, email = user.Email,
rank = (int)user.Rank, rank = (int)user.Rank,
banned = user.Banned, banned = user.Banned,
banReason = user.BanReason ?? "",
dateOfBirth = user.DateOfBirth.ToString("yyyy-MM-dd"), dateOfBirth = user.DateOfBirth.ToString("yyyy-MM-dd"),
dateJoined = user.Joined.ToString("yyyy-MM-dd HH:mm:ss"), dateJoined = user.Joined.ToString("yyyy-MM-dd HH:mm:ss"),
registrationIp = user.RegistrationIP.ToString(), registrationIp = user.RegistrationIP.ToString(),

View file

@ -63,7 +63,7 @@ public static class DeveloperRoutes
// Check developer status // Check developer status
var user = await Program.Database.GetUser(session.Username) ?? var user = await Program.Database.GetUser(session.Username) ??
throw new Exception("Unable to get user from session"); throw new Exception("Unable to get user from session");
if (!user.Developer && user.Rank != Rank.Admin) if (!user.Developer && user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
{ {
context.Response.StatusCode = 403; context.Response.StatusCode = 403;
return Results.Json(new CreateBotResponse return Results.Json(new CreateBotResponse
@ -72,8 +72,8 @@ public static class DeveloperRoutes
error = "You must be an approved developer to create and manage bots." error = "You must be an approved developer to create and manage bots."
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// owner can only be specified by admins // owner can only be specified by admins and moderators
if (payload.owner != null && user.Rank != Rank.Admin) if (payload.owner != null && user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
{ {
context.Response.StatusCode = 403; context.Response.StatusCode = 403;
return Results.Json(new ListBotsResponse return Results.Json(new ListBotsResponse
@ -84,7 +84,7 @@ public static class DeveloperRoutes
} }
// Get bots // Get bots
// If the user is not an admin, they can only see their own 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 var bots = (await Program.Database.ListBots(payload.owner ?? ((user.Rank == Rank.Admin || user.Rank == Rank.Moderator) ? null : user.Username))).Select(bot => new ListBot
{ {
id = (int)bot.Id, id = (int)bot.Id,
username = bot.Username, username = bot.Username,

View file

@ -0,0 +1,9 @@
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
public class BanUserPayload
{
public string token { get; set; }
public string username { get; set; }
public bool banned { get; set; }
public string reason { get; set; }
}

View file

@ -0,0 +1,9 @@
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
public class IPBanPayload
{
public string session { get; set; }
public string ip { get; set; }
public bool banned { get; set; }
public string reason { get; set; }
}

View file

@ -15,6 +15,7 @@ public class AdminUser
public string email { get; set; } public string email { get; set; }
public int rank { get; set; } public int rank { get; set; }
public bool banned { get; set; } public bool banned { get; set; }
public string banReason { get; set; }
public string dateOfBirth { get; set; } public string dateOfBirth { get; set; }
public string dateJoined { get; set; } public string dateJoined { get; set; }
public string registrationIp { get; set; } public string registrationIp { get; set; }

View file

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

View file

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

View file

@ -4,6 +4,8 @@ public class JoinResponse
{ {
public bool success { get; set; } public bool success { get; set; }
public bool clientSuccess { get; set; } = false; public bool clientSuccess { get; set; } = false;
public bool? banned { get; set; } = null;
public string? banReason { get; set; }
public string? error { get; set; } public string? error { get; set; }
public string? username { get; set; } public string? username { get; set; }
public Rank? rank { get; set; } public Rank? rank { get; set; }

View file

@ -556,7 +556,9 @@ public static class Routes
{ {
success = true, success = true,
clientSuccess = false, clientSuccess = false,
error = "You are banned" error = "Banned",
banned = true,
banReason = ban.Reason
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Check if session is valid // Check if session is valid
@ -592,7 +594,9 @@ public static class Routes
{ {
success = true, success = true,
clientSuccess = false, clientSuccess = false,
error = "You are banned", banned = true,
error = "Banned",
banReason = user.BanReason
}, Utilities.JsonSerializerOptions); }, Utilities.JsonSerializerOptions);
} }
// Update session // Update session
@ -1026,33 +1030,29 @@ public static class Routes
}); });
} }
// Create the account // Create the account
string? token = null;
if (Program.Config.Registration.EmailVerificationRequired) if (Program.Config.Registration.EmailVerificationRequired)
{ {
var code = Program.Random.Next(10000000, 99999999).ToString(); var code = Program.Random.Next(10000000, 99999999).ToString();
await Program.Database.RegisterAccount(payload.username, payload.email, dob, payload.password, false, ip,code); await Program.Database.RegisterAccount(payload.username, payload.email, dob, payload.password, false, ip,code);
await Program.Mailer.SendVerificationCode(payload.username, payload.email, code); await Program.Mailer.SendVerificationCode(payload.username, payload.email, code);
return Results.Json(new RegisterResponse
{
success = true,
verificationRequired = true,
email = payload.email,
username = payload.username
}, Utilities.JsonSerializerOptions);
} }
else else
{ {
await Program.Database.RegisterAccount(payload.username, payload.email, dob, payload.password, true, ip, null); await Program.Database.RegisterAccount(payload.username, payload.email, dob, payload.password, true, ip, null);
var token = Utilities.RandomString(32); token = Utilities.RandomString(32);
await Program.Database.CreateSession(payload.username, token, ip); await Program.Database.CreateSession(payload.username, token, ip);
return Results.Json(new RegisterResponse
{
success = true,
verificationRequired = false,
email = payload.email,
username = payload.username,
sessionToken = token
}, Utilities.JsonSerializerOptions);
} }
// If this is the first user, make them an admin
if (await Program.Database.CountUsers() == 1) await Program.Database.UpdateUser(payload.username, newRank: (int)Rank.Admin);
return Results.Json(new RegisterResponse
{
success = true,
verificationRequired = Program.Config.Registration.EmailVerificationRequired,
email = payload.email,
username = payload.username,
sessionToken = token
}, Utilities.JsonSerializerOptions);
} }
private static IResult HandleInfo(HttpContext context) private static IResult HandleInfo(HttpContext context)

View file

@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Reflection;
using Computernewb.CollabVMAuthServer.HTTP; using Computernewb.CollabVMAuthServer.HTTP;
using Tomlet; using Tomlet;
@ -14,7 +15,8 @@ public class Program
public static readonly Random Random = new Random(); public static readonly Random Random = new Random();
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
Utilities.Log(LogLevel.INFO, "CollabVM Authentication Server starting up"); var ver = Assembly.GetExecutingAssembly().GetName().Version;
Utilities.Log(LogLevel.INFO, $"CollabVM Authentication Server v{ver.Major}.{ver.Minor}.{ver.Revision} starting up");
// Read config.toml // Read config.toml
string configraw; string configraw;
try try
@ -39,8 +41,20 @@ public class Program
} }
// Initialize database // Initialize database
Database = new Database(Config.MySQL); Database = new Database(Config.MySQL);
await Database.Init(); // Get version before initializing
int dbversion = await Database.GetDatabaseVersion();
Utilities.Log(LogLevel.INFO, "Connected to database"); Utilities.Log(LogLevel.INFO, "Connected to database");
Utilities.Log(LogLevel.INFO, dbversion == -1 ? "Initializing tables..." : $"Database version: {dbversion}");
await Database.Init();
// If database was version 0, that should now be set, as versioning did not exist then
if (dbversion == 0) await Database.SetDatabaseVersion(0);
// If database was -1, that means it was just initialized and we should set it to the current version
if (dbversion == -1) await Database.SetDatabaseVersion(DatabaseUpdate.CurrentVersion);
// Perform any necessary database updates
await DatabaseUpdate.Update(Database);
var uc = await Database.CountUsers();
Utilities.Log(LogLevel.INFO, $"{uc} users in database");
if (uc == 0) Utilities.Log(LogLevel.WARN, "No users in database, first user will be promoted to admin");
// Create mailer // Create mailer
if (!Config.SMTP.Enabled && Config.Registration.EmailVerificationRequired) if (!Config.SMTP.Enabled && Config.Registration.EmailVerificationRequired)
{ {

View file

@ -14,6 +14,7 @@ public class User
public string? PasswordResetCode { get; set; } public string? PasswordResetCode { get; set; }
public Rank Rank { get; set; } public Rank Rank { get; set; }
public bool Banned { get; set; } public bool Banned { get; set; }
public string? BanReason { get; set; }
public IPAddress RegistrationIP { get; set; } public IPAddress RegistrationIP { get; set; }
public DateTime Joined { get; set; } public DateTime Joined { get; set; }
public bool Developer { get; set; } public bool Developer { get; set; }

View file

@ -63,7 +63,7 @@ public static class Utilities
case LogLevel.WARN: case LogLevel.WARN:
case LogLevel.ERROR: case LogLevel.ERROR:
case LogLevel.FATAL: case LogLevel.FATAL:
Console.Error.Write(logstr.ToString()); Console.Error.WriteLine(logstr.ToString());
break; break;
} }
} }