major refactor:
- Now uses EntityFrameworkCore for database operations - HTTP handlers have all been refactored to use ASP.NET MVC controllers, and generally to be more idiomatic and remove copied boilerplate ugliness - Authentication/Authorization refactored to use native ASP.NET core handlers - Switch to Microsoft.Extensions.Logging instead of handrolled logging method
This commit is contained in:
26
CollabVMAuthServer/AuthServerContext.cs
Normal file
26
CollabVMAuthServer/AuthServerContext.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.CommandLine.Binding;
|
||||||
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
|
public class AuthServerContext {
|
||||||
|
public required IConfig Config { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AuthServerCliOptionsBinder : BinderBase<AuthServerContext> {
|
||||||
|
private readonly Option<string> _configPathOption;
|
||||||
|
|
||||||
|
public AuthServerCliOptionsBinder(Option<string> configPathOption) {
|
||||||
|
this._configPathOption = configPathOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override AuthServerContext GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var configPath = bindingContext.ParseResult.GetValueForOption(_configPathOption)!;
|
||||||
|
// Load config file
|
||||||
|
var config = IConfig.Load(configPath);
|
||||||
|
|
||||||
|
return new AuthServerContext {
|
||||||
|
Config = config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,16 @@
|
|||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
<PackageReference Include="MailKit" Version="4.12.0" />
|
<PackageReference Include="MailKit" Version="4.12.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||||
<PackageReference Include="Samboy063.Tomlet" Version="6.0.0" />
|
<PackageReference Include="Samboy063.Tomlet" Version="6.0.0" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
public static class Cron
|
public class Cron
|
||||||
{
|
{
|
||||||
private static Timer timer = new Timer();
|
private readonly DbContextOptions<CollabVMAuthDbContext> _dbContextOptions;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
public Cron(DbContextOptions<CollabVMAuthDbContext> dbContextOptions) {
|
||||||
|
this._dbContextOptions = dbContextOptions;
|
||||||
|
this._logger = LoggerFactory.Create(Utilities.ConfigureLogging).CreateLogger<Cron>();
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task Start()
|
private Timer timer = new Timer();
|
||||||
|
|
||||||
|
public async Task Start()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
timer.Interval = 1000 * 60; // 60 seconds
|
timer.Interval = 1000 * 60; // 60 seconds
|
||||||
@@ -21,41 +32,37 @@ public static class Cron
|
|||||||
timer.Start();
|
timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
timer.Interval = 1000 * 60 * 10;
|
timer.Interval = 1000 * 60 * 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task RunAll()
|
public async Task RunAll()
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.INFO, "Running all cron jobs");
|
_logger.LogDebug("Running all cron jobs");
|
||||||
var t = new List<Task>();
|
var t = new List<Task>();
|
||||||
t.Add(PurgeOldSessions());
|
t.Add(PurgeOldSessions());
|
||||||
if (Program.Config.Registration.EmailVerificationRequired) t.Add(ExpireAccounts());
|
if (Program.Config.Registration!.EmailVerificationRequired) t.Add(ExpireAccounts());
|
||||||
await Task.WhenAll(t);
|
await Task.WhenAll(t);
|
||||||
Utilities.Log(LogLevel.INFO, "Finished running all cron jobs");
|
_logger.LogDebug("Finished running all cron jobs");
|
||||||
}
|
}
|
||||||
// Expire unverified accounts after 2 days. Don't purge if the code is null
|
// Expire unverified accounts after 2 days. Don't purge if the code is null
|
||||||
public static async Task ExpireAccounts()
|
public async Task ExpireAccounts()
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.INFO, "Purging unverified accounts");
|
_logger.LogDebug("Purging unverified accounts");
|
||||||
var minDate = DateTime.UtcNow - TimeSpan.FromDays(2);
|
using var dbContext = new CollabVMAuthDbContext(_dbContextOptions);
|
||||||
int a = await Program.Database.ExecuteNonQuery("DELETE FROM users WHERE email_verified = 0 AND created < @minDate AND email_verification_code IS NOT NULL",
|
dbContext.Users.RemoveRange(dbContext.Users.Where(u => (!u.EmailVerified) && u.EmailVerificationCode != null && (u.Created < DateTime.UtcNow.AddDays(-2))));
|
||||||
[
|
var a = await dbContext.SaveChangesAsync();
|
||||||
new KeyValuePair<string, object>("minDate", minDate)
|
_logger.LogInformation("Purged {a} unverified accounts", a);
|
||||||
]);
|
|
||||||
Utilities.Log(LogLevel.INFO, $"Purged {a} unverified accounts");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task PurgeOldSessions()
|
public async Task PurgeOldSessions()
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.INFO, "Purging old sessions");
|
_logger.LogDebug("Purging old sessions");
|
||||||
var expiryDate = DateTime.UtcNow - TimeSpan.FromDays(Program.Config.Accounts.SessionExpiryDays);
|
using var dbContext = new CollabVMAuthDbContext(_dbContextOptions);
|
||||||
int a = await Program.Database.ExecuteNonQuery("DELETE FROM sessions WHERE last_used < @expiryDate",
|
dbContext.Sessions.RemoveRange(dbContext.Sessions.Where(s => s.LastUsed < DateTime.UtcNow - TimeSpan.FromDays(Program.Config.Accounts!.SessionExpiryDays)));
|
||||||
[
|
var a = await dbContext.SaveChangesAsync();
|
||||||
new KeyValuePair<string, object>("expiryDate", expiryDate)
|
_logger.LogInformation("Purged {a} old sessions", a);
|
||||||
]);
|
|
||||||
Utilities.Log(LogLevel.INFO, $"Purged {a} old sessions");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Isopoh.Cryptography.Argon2;
|
|
||||||
using MySqlConnector;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
|
||||||
|
|
||||||
public class Database
|
|
||||||
{
|
|
||||||
private readonly string connectionString;
|
|
||||||
|
|
||||||
public Database(MySQLConfig config)
|
|
||||||
{
|
|
||||||
connectionString = new MySqlConnectionStringBuilder
|
|
||||||
{
|
|
||||||
Server = config.Host,
|
|
||||||
UserID = config.Username,
|
|
||||||
Password = config.Password,
|
|
||||||
Database = config.Database
|
|
||||||
}.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Init()
|
|
||||||
{
|
|
||||||
await using var conn = new MySqlConnection(connectionString);
|
|
||||||
await conn.OpenAsync();
|
|
||||||
await using var cmd = conn.CreateCommand();
|
|
||||||
cmd.CommandText = """
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(20) NOT NULL UNIQUE KEY,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL UNIQUE KEY,
|
|
||||||
date_of_birth DATE NOT NULL,
|
|
||||||
email_verified BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
email_verification_code CHAR(8) DEFAULT NULL,
|
|
||||||
password_reset_code CHAR(8) DEFAULT NULL,
|
|
||||||
cvm_rank INT UNSIGNED NOT NULL DEFAULT 1,
|
|
||||||
banned BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
ban_reason TEXT DEFAULT NULL,
|
|
||||||
registration_ip VARBINARY(16) NOT NULL,
|
|
||||||
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
developer BOOLEAN NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
""";
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
cmd.CommandText = """
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
token CHAR(32) NOT NULL PRIMARY KEY,
|
|
||||||
username VARCHAR(20) NOT NULL,
|
|
||||||
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_used TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_ip VARBINARY(16) NOT NULL,
|
|
||||||
FOREIGN KEY (username) REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
""";
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
// banned_by being NULL means the ban was automatic
|
|
||||||
cmd.CommandText = """
|
|
||||||
CREATE TABLE IF NOT EXISTS ip_bans (
|
|
||||||
ip VARBINARY(16) NOT NULL PRIMARY KEY,
|
|
||||||
reason TEXT NOT NULL,
|
|
||||||
banned_by VARCHAR(20) DEFAULT NULL,
|
|
||||||
banned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
""";
|
|
||||||
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();
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (username == null && email == null)
|
|
||||||
throw new ArgumentException("username or email must be provided");
|
|
||||||
await using var conn = new MySqlConnection(connectionString);
|
|
||||||
await conn.OpenAsync();
|
|
||||||
await using var cmd = conn.CreateCommand();
|
|
||||||
if (username != null)
|
|
||||||
{
|
|
||||||
cmd.CommandText = "SELECT * FROM users WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
}
|
|
||||||
else if (email != null)
|
|
||||||
{
|
|
||||||
cmd.CommandText = "SELECT * FROM users WHERE email = @email";
|
|
||||||
cmd.Parameters.AddWithValue("@email", email);
|
|
||||||
}
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
if (!await reader.ReadAsync())
|
|
||||||
return null;
|
|
||||||
return 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"),
|
|
||||||
BanReason = reader.IsDBNull("ban_reason") ? null : reader.GetString("ban_reason"),
|
|
||||||
RegistrationIP = new IPAddress(reader.GetFieldValue<byte[]>("registration_ip")),
|
|
||||||
Joined = reader.GetDateTime("created"),
|
|
||||||
Developer = reader.GetBoolean("developer")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RegisterAccount(string username, string email, DateOnly dateOfBirth, string password, bool verified, IPAddress ip,
|
|
||||||
string? verificationcode = null)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = """
|
|
||||||
INSERT INTO users
|
|
||||||
(username, password, email, date_of_birth, email_verified, email_verification_code, registration_ip)
|
|
||||||
VALUES
|
|
||||||
(@username, @password, @email, @date_of_birth, @email_verified, @email_verification_code, @registration_ip)
|
|
||||||
""";
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
cmd.Parameters.AddWithValue("@password", Argon2.Hash(password));
|
|
||||||
cmd.Parameters.AddWithValue("@email", email);
|
|
||||||
cmd.Parameters.Add("@date_of_birth", MySqlDbType.Date).Value = dateOfBirth;
|
|
||||||
cmd.Parameters.AddWithValue("@email_verified", verified);
|
|
||||||
cmd.Parameters.AddWithValue("@email_verification_code", verificationcode);
|
|
||||||
cmd.Parameters.AddWithValue("@registration_ip", ip.GetAddressBytes());
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetUserVerified(string username, bool verified)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "UPDATE users SET email_verified = @verified WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@verified", verified);
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetVerificationCode(string username, string code)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "UPDATE users SET email_verification_code = @code WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@code", code);
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateSession(string username, string token, IPAddress ip)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "INSERT INTO sessions (token, username, last_ip) VALUES (@token, @username, @ip)";
|
|
||||||
cmd.Parameters.AddWithValue("@token", token);
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
cmd.Parameters.AddWithValue("@ip", ip.GetAddressBytes());
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Session[]> GetSessions(string username)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT * FROM sessions WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
var sessions = new List<Session>();
|
|
||||||
while (await reader.ReadAsync())
|
|
||||||
{
|
|
||||||
sessions.Add(new Session
|
|
||||||
{
|
|
||||||
Token = reader.GetString("token"),
|
|
||||||
Username = reader.GetString("username"),
|
|
||||||
Created = reader.GetDateTime("created"),
|
|
||||||
LastUsed = reader.GetDateTime("last_used"),
|
|
||||||
LastIP = new IPAddress(reader.GetFieldValue<byte[]>("last_ip"))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return sessions.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Session?> GetSession(string token)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT * FROM sessions WHERE token = @token";
|
|
||||||
cmd.Parameters.AddWithValue("@token", token);
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
if (!await reader.ReadAsync())
|
|
||||||
return null;
|
|
||||||
return new Session
|
|
||||||
{
|
|
||||||
Token = reader.GetString("token"),
|
|
||||||
Username = reader.GetString("username"),
|
|
||||||
Created = reader.GetDateTime("created"),
|
|
||||||
LastUsed = reader.GetDateTime("last_used"),
|
|
||||||
LastIP = new IPAddress(reader.GetFieldValue<byte[]>("last_ip"))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RevokeSession(string token)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "DELETE FROM sessions WHERE token = @token";
|
|
||||||
cmd.Parameters.AddWithValue("@token", token);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RevokeAllSessions(string username)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "DELETE FROM sessions WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateSessionLastUsed(string token, IPAddress ip)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "UPDATE sessions SET last_used = CURRENT_TIMESTAMP, last_ip = @ip WHERE token = @token";
|
|
||||||
cmd.Parameters.AddWithValue("@token", token);
|
|
||||||
cmd.Parameters.AddWithValue("@ip", ip.GetAddressBytes());
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
var updates = new List<string>();
|
|
||||||
if (newUsername != null)
|
|
||||||
{
|
|
||||||
updates.Add("username = @newUsername");
|
|
||||||
cmd.Parameters.AddWithValue("@newUsername", newUsername);
|
|
||||||
}
|
|
||||||
if (newPassword != null)
|
|
||||||
{
|
|
||||||
updates.Add("password = @newPassword");
|
|
||||||
cmd.Parameters.AddWithValue("@newPassword", Argon2.Hash(newPassword));
|
|
||||||
}
|
|
||||||
if (newEmail != null)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task BanIP(IPAddress ip, string reason, string? bannedBy = null)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "INSERT INTO ip_bans (ip, reason, banned_by) VALUES (@ip, @reason, @bannedBy)";
|
|
||||||
cmd.Parameters.AddWithValue("@ip", ip.GetAddressBytes());
|
|
||||||
cmd.Parameters.AddWithValue("@reason", reason);
|
|
||||||
cmd.Parameters.AddWithValue("@bannedBy", bannedBy);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnbanIP(IPAddress ip)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "DELETE FROM ip_bans WHERE ip = @ip";
|
|
||||||
cmd.Parameters.AddWithValue("@ip", ip.GetAddressBytes());
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IPBan?> CheckIPBan(IPAddress ip)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT * FROM ip_bans WHERE ip = @ip";
|
|
||||||
cmd.Parameters.AddWithValue("@ip", ip.GetAddressBytes());
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync();
|
|
||||||
if (!await reader.ReadAsync())
|
|
||||||
return null;
|
|
||||||
return new IPBan
|
|
||||||
{
|
|
||||||
IP = new IPAddress(reader.GetFieldValue<byte[]>("ip")),
|
|
||||||
Reason = reader.GetString("reason"),
|
|
||||||
BannedBy = reader.IsDBNull("banned_by") ? null : reader.GetString("banned_by"),
|
|
||||||
BannedAt = reader.GetDateTime("banned_at")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetPasswordResetCode(string username, string? code)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = "UPDATE users SET password_reset_code = @code WHERE username = @username";
|
|
||||||
cmd.Parameters.AddWithValue("@code", code);
|
|
||||||
cmd.Parameters.AddWithValue("@username", username);
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User[]> ListUsers(string? filterUsername = null, IPAddress? filterIp = 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);
|
|
||||||
}
|
|
||||||
if (filterIp != null)
|
|
||||||
{
|
|
||||||
where.Add("registration_ip = @filterIp");
|
|
||||||
cmd.Parameters.AddWithValue("@filterIp", filterIp.GetAddressBytes());
|
|
||||||
}
|
|
||||||
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"),
|
|
||||||
BanReason = reader.IsDBNull("ban_reason") ? null : reader.GetString("ban_reason"),
|
|
||||||
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")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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<int> ExecuteNonQuery(string query, KeyValuePair<string, object>[]? parameters = null)
|
|
||||||
{
|
|
||||||
await using var db = new MySqlConnection(connectionString);
|
|
||||||
await db.OpenAsync();
|
|
||||||
await using var cmd = db.CreateCommand();
|
|
||||||
cmd.CommandText = query;
|
|
||||||
if (parameters != null) foreach (KeyValuePair<string, object> param in parameters)
|
|
||||||
{
|
|
||||||
cmd.Parameters.AddWithValue(param.Key, param.Value);
|
|
||||||
}
|
|
||||||
return 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
174
CollabVMAuthServer/Database/CollabVMAuthDbContext.cs
Normal file
174
CollabVMAuthServer/Database/CollabVMAuthDbContext.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database;
|
||||||
|
|
||||||
|
public partial class CollabVMAuthDbContext : DbContext
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618
|
||||||
|
public CollabVMAuthDbContext(DbContextOptions<CollabVMAuthDbContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
|
public virtual DbSet<Bot> Bots { get; set; }
|
||||||
|
|
||||||
|
public virtual DbSet<IpBan> IpBans { get; set; }
|
||||||
|
public virtual DbSet<Session> Sessions { get; set; }
|
||||||
|
|
||||||
|
public virtual DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder
|
||||||
|
.UseCollation("utf8mb4_unicode_ci")
|
||||||
|
.HasCharSet("utf8mb4");
|
||||||
|
|
||||||
|
modelBuilder.Entity<Bot>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
||||||
|
|
||||||
|
entity.ToTable("bots");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Owner, "owner");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Token, "token").IsUnique();
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Username, "username").IsUnique();
|
||||||
|
|
||||||
|
entity.Property(e => e.Id)
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
entity.Property(e => e.Created)
|
||||||
|
.HasDefaultValueSql("current_timestamp()")
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created");
|
||||||
|
entity.Property(e => e.CvmRank)
|
||||||
|
.HasDefaultValueSql("'1'")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank");
|
||||||
|
entity.Property(e => e.Owner)
|
||||||
|
.HasColumnName("owner");
|
||||||
|
entity.Property(e => e.Token)
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.IsFixedLength()
|
||||||
|
.HasColumnName("token");
|
||||||
|
entity.Property(e => e.Username)
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
entity.HasOne(d => d.OwnerNavigation).WithMany(p => p.Bots)
|
||||||
|
.HasPrincipalKey(p => p.Id)
|
||||||
|
.HasForeignKey(d => d.Owner)
|
||||||
|
.HasConstraintName("owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<IpBan>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Ip).HasName("PRIMARY");
|
||||||
|
|
||||||
|
entity.ToTable("ip_bans");
|
||||||
|
|
||||||
|
entity.Property(e => e.Ip)
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnName("ip");
|
||||||
|
entity.Property(e => e.BannedAt)
|
||||||
|
.HasDefaultValueSql("current_timestamp()")
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("banned_at");
|
||||||
|
entity.Property(e => e.BannedBy)
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnName("banned_by");
|
||||||
|
entity.Property(e => e.Reason)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reason");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Session>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Token).HasName("PRIMARY");
|
||||||
|
|
||||||
|
entity.ToTable("sessions");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.UserId, "user");
|
||||||
|
|
||||||
|
entity.Property(e => e.Token)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsFixedLength()
|
||||||
|
.HasColumnName("token");
|
||||||
|
entity.Property(e => e.Created)
|
||||||
|
.HasDefaultValueSql("current_timestamp()")
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created");
|
||||||
|
entity.Property(e => e.LastIp)
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnName("last_ip");
|
||||||
|
entity.Property(e => e.LastUsed)
|
||||||
|
.HasDefaultValueSql("current_timestamp()")
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("last_used");
|
||||||
|
entity.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user");
|
||||||
|
|
||||||
|
entity.HasOne(d => d.UserNavigation).WithMany(p => p.Sessions)
|
||||||
|
.HasPrincipalKey(p => p.Id)
|
||||||
|
.HasForeignKey(d => d.UserId)
|
||||||
|
.HasConstraintName("user");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
||||||
|
|
||||||
|
entity.ToTable("users");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Email, "email").IsUnique();
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Username, "username").IsUnique();
|
||||||
|
|
||||||
|
entity.Property(e => e.Id)
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
entity.Property(e => e.BanReason)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("ban_reason");
|
||||||
|
entity.Property(e => e.Banned).HasColumnName("banned");
|
||||||
|
entity.Property(e => e.Created)
|
||||||
|
.HasDefaultValueSql("current_timestamp()")
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created");
|
||||||
|
entity.Property(e => e.CvmRank)
|
||||||
|
.HasDefaultValueSql("'1'")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank");
|
||||||
|
entity.Property(e => e.DateOfBirth).HasColumnName("date_of_birth");
|
||||||
|
entity.Property(e => e.Developer).HasColumnName("developer");
|
||||||
|
entity.Property(e => e.Email)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("email");
|
||||||
|
entity.Property(e => e.EmailVerificationCode)
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.IsFixedLength()
|
||||||
|
.HasColumnName("email_verification_code");
|
||||||
|
entity.Property(e => e.EmailVerified).HasColumnName("email_verified");
|
||||||
|
entity.Property(e => e.Password)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("password");
|
||||||
|
entity.Property(e => e.PasswordResetCode)
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.IsFixedLength()
|
||||||
|
.HasColumnName("password_reset_code");
|
||||||
|
entity.Property(e => e.RegistrationIp)
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnName("registration_ip");
|
||||||
|
entity.Property(e => e.Username)
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnName("username");
|
||||||
|
});
|
||||||
|
|
||||||
|
OnModelCreatingPartial(modelBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database;
|
||||||
|
|
||||||
|
public class DesignTimeCollabVMAuthDbContextFactory : IDesignTimeDbContextFactory<CollabVMAuthDbContext> {
|
||||||
|
public CollabVMAuthDbContext CreateDbContext(string[] args) {
|
||||||
|
return new CollabVMAuthDbContext(
|
||||||
|
new DbContextOptionsBuilder<CollabVMAuthDbContext>()
|
||||||
|
.UseMySql(MariaDbServerVersion.LatestSupportedServerVersion).Options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
CollabVMAuthServer/Database/LegacyDbMigrator.cs
Normal file
73
CollabVMAuthServer/Database/LegacyDbMigrator.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database;
|
||||||
|
|
||||||
|
public static class LegacyDbMigrator {
|
||||||
|
/// <summary>
|
||||||
|
/// The initial database migration that a pre-EF database will already be compatible with
|
||||||
|
/// </summary>
|
||||||
|
public const string INITIAL_MIGRATION_NAME = "20250505224256_InitialDbModel";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a database was initialized by the legacy pre-EF methods. If so, create the migrations table and manually add the initial migration
|
||||||
|
/// </summary>
|
||||||
|
public static async Task CheckAndMigrate(CollabVMAuthDbContext context) {
|
||||||
|
var logger = LoggerFactory.Create(Utilities.ConfigureLogging).CreateLogger("Computernewb.CollabVMAuthServer.Database.LegacyDbMigrator");
|
||||||
|
// Check if initial migration is pending
|
||||||
|
if ((await context.Database.GetAppliedMigrationsAsync()).Contains(INITIAL_MIGRATION_NAME)) {
|
||||||
|
logger.LogDebug("Initial migration already applied, skipping legacy db check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var conn = context.Database.GetDbConnection();
|
||||||
|
// Ensure the connection is open
|
||||||
|
if (conn.State != ConnectionState.Open) {
|
||||||
|
logger.LogDebug("Opening DB connection");
|
||||||
|
await conn.OpenAsync();
|
||||||
|
}
|
||||||
|
// Create command
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
// Check if meta table exists
|
||||||
|
cmd.CommandText = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'meta'";
|
||||||
|
// If meta table doesn't exist, db is uninitialized
|
||||||
|
if ((long)(await cmd.ExecuteScalarAsync() ?? 0) == 0) {
|
||||||
|
logger.LogDebug("Database is uninitialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check database version
|
||||||
|
cmd.CommandText = "SELECT val FROM meta WHERE setting = 'db_version'";
|
||||||
|
var dbVer = (string?) await cmd.ExecuteScalarAsync();
|
||||||
|
if (dbVer != "1") {
|
||||||
|
// 1 was the only version ever used in the old format, so this should not happen
|
||||||
|
throw new InvalidOperationException($"Invalid database state, cannot automatically migrate (Expected DB version `1`, got `{dbVer}`)");
|
||||||
|
}
|
||||||
|
// Database can be migrated
|
||||||
|
logger.LogDebug("Legacy database schema detected. Automatically initializing migrations table");
|
||||||
|
// Manually create migrations table
|
||||||
|
var historyRepo = context.Database.GetService<IHistoryRepository>();
|
||||||
|
cmd.CommandText = historyRepo.GetCreateIfNotExistsScript();
|
||||||
|
logger.LogDebug("Migrations table create script: {cmd}", cmd.CommandText);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
// Insert row for initial migration
|
||||||
|
cmd.CommandText = historyRepo.GetInsertScript(
|
||||||
|
new HistoryRow(
|
||||||
|
INITIAL_MIGRATION_NAME,
|
||||||
|
typeof(DbContext).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion
|
||||||
|
)
|
||||||
|
);
|
||||||
|
logger.LogDebug("Migrations table insert script: {cmd}", cmd.CommandText);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
logger.LogInformation("Successfully initialized migrations table");
|
||||||
|
// Drop meta table
|
||||||
|
cmd.CommandText = "DROP TABLE meta;";
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
logger.LogInformation("Dropped legacy meta table");
|
||||||
|
}
|
||||||
|
}
|
||||||
275
CollabVMAuthServer/Database/Migrations/20250505224256_InitialDbModel.Designer.cs
generated
Normal file
275
CollabVMAuthServer/Database/Migrations/20250505224256_InitialDbModel.Designer.cs
generated
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CollabVMAuthDbContext))]
|
||||||
|
[Migration("20250505224256_InitialDbModel")]
|
||||||
|
partial class InitialDbModel
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.UseCollation("utf8mb4_unicode_ci")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "utf8mb4");
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<string>("Owner")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("owner");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("char(64)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Owner" }, "owner");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Token" }, "token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bots", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.IpBan", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Ip")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("BannedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("banned_at")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<string>("BannedBy")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("banned_by");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reason");
|
||||||
|
|
||||||
|
b.HasKey("Ip")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.ToTable("ip_bans", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("char(32)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<byte[]>("LastIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("last_ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUsed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("last_used")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Token")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username");
|
||||||
|
|
||||||
|
b.ToTable("sessions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BanReason")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("ban_reason");
|
||||||
|
|
||||||
|
b.Property<bool>("Banned")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("banned");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateOfBirth")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date_of_birth");
|
||||||
|
|
||||||
|
b.Property<bool>("Developer")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("developer");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("EmailVerificationCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("email_verification_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<bool>("EmailVerified")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("email_verified");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("password");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordResetCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("password_reset_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<byte[]>("RegistrationIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("registration_ip");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Email" }, "email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "OwnerNavigation")
|
||||||
|
.WithMany("Bots")
|
||||||
|
.HasForeignKey("Owner")
|
||||||
|
.HasPrincipalKey("Username")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("bots_ibfk_1");
|
||||||
|
|
||||||
|
b.Navigation("OwnerNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "UsernameNavigation")
|
||||||
|
.WithMany("Sessions")
|
||||||
|
.HasForeignKey("Username")
|
||||||
|
.HasPrincipalKey("Username")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("sessions_ibfk_1");
|
||||||
|
|
||||||
|
b.Navigation("UsernameNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bots");
|
||||||
|
|
||||||
|
b.Navigation("Sessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialDbModel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ip_bans",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ip = table.Column<byte[]>(type: "varbinary(16)", maxLength: 16, nullable: false),
|
||||||
|
reason = table.Column<string>(type: "text", nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
banned_by = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: true, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
banned_at = table.Column<DateTime>(type: "timestamp", nullable: false, defaultValueSql: "current_timestamp()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.ip);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<uint>(type: "int(10) unsigned", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
username = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
password = table.Column<string>(type: "text", nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
email = table.Column<string>(type: "text", nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
date_of_birth = table.Column<DateOnly>(type: "date", nullable: false),
|
||||||
|
email_verified = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||||
|
email_verification_code = table.Column<string>(type: "char(8)", fixedLength: true, maxLength: 8, nullable: true, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
password_reset_code = table.Column<string>(type: "char(8)", fixedLength: true, maxLength: 8, nullable: true, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
cvm_rank = table.Column<uint>(type: "int(10) unsigned", nullable: false, defaultValueSql: "'1'"),
|
||||||
|
banned = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||||
|
ban_reason = table.Column<string>(type: "text", nullable: true, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
registration_ip = table.Column<byte[]>(type: "varbinary(16)", maxLength: 16, nullable: false),
|
||||||
|
created = table.Column<DateTime>(type: "timestamp", nullable: false, defaultValueSql: "current_timestamp()"),
|
||||||
|
developer = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.id);
|
||||||
|
table.UniqueConstraint("username", x => x.username);
|
||||||
|
table.UniqueConstraint("email", x => x.email);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "bots",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<uint>(type: "int(10) unsigned", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
username = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
token = table.Column<string>(type: "char(64)", fixedLength: true, maxLength: 64, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
cvm_rank = table.Column<uint>(type: "int(10) unsigned", nullable: false, defaultValueSql: "'1'"),
|
||||||
|
owner = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
created = table.Column<DateTime>(type: "timestamp", nullable: false, defaultValueSql: "current_timestamp()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.id);
|
||||||
|
table.UniqueConstraint("username", x => x.username);
|
||||||
|
table.UniqueConstraint("token", x => x.token);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "bots_ibfk_1",
|
||||||
|
column: x => x.owner,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "username",
|
||||||
|
onUpdate: ReferentialAction.Cascade,
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "owner",
|
||||||
|
column: "owner",
|
||||||
|
table: "bots"
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "sessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
token = table.Column<string>(type: "char(32)", fixedLength: true, maxLength: 32, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
username = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false, collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
created = table.Column<DateTime>(type: "timestamp", nullable: false, defaultValueSql: "current_timestamp()"),
|
||||||
|
last_used = table.Column<DateTime>(type: "timestamp", nullable: false, defaultValueSql: "current_timestamp()"),
|
||||||
|
last_ip = table.Column<byte[]>(type: "varbinary(16)", maxLength: 16, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.token);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "sessions_ibfk_1",
|
||||||
|
column: x => x.username,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "username",
|
||||||
|
onUpdate: ReferentialAction.Cascade,
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_unicode_ci");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "username",
|
||||||
|
column: "username",
|
||||||
|
table: "sessions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ip_bans");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
270
CollabVMAuthServer/Database/Migrations/20250506060015_UseUserIdAsForeignKey.Designer.cs
generated
Normal file
270
CollabVMAuthServer/Database/Migrations/20250506060015_UseUserIdAsForeignKey.Designer.cs
generated
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CollabVMAuthDbContext))]
|
||||||
|
[Migration("20250506060015_UseUserIdAsForeignKey")]
|
||||||
|
partial class UseUserIdAsForeignKey
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.UseCollation("utf8mb4_unicode_ci")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "utf8mb4");
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<uint>("Owner")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("owner");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("char(64)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Owner" }, "owner");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Token" }, "token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bots", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.IpBan", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Ip")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("BannedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("banned_at")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<string>("BannedBy")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("banned_by");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reason");
|
||||||
|
|
||||||
|
b.HasKey("Ip")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.ToTable("ip_bans", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("char(32)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<byte[]>("LastIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("last_ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUsed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("last_used")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("UserId")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("user");
|
||||||
|
|
||||||
|
b.HasKey("Token")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "UserId" }, "user");
|
||||||
|
|
||||||
|
b.ToTable("sessions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BanReason")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("ban_reason");
|
||||||
|
|
||||||
|
b.Property<bool>("Banned")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("banned");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateOfBirth")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date_of_birth");
|
||||||
|
|
||||||
|
b.Property<bool>("Developer")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("developer");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("EmailVerificationCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("email_verification_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<bool>("EmailVerified")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("email_verified");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("password");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordResetCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("password_reset_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<byte[]>("RegistrationIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("registration_ip");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Email" }, "email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("username1");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "OwnerNavigation")
|
||||||
|
.WithMany("Bots")
|
||||||
|
.HasForeignKey("Owner")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("owner");
|
||||||
|
|
||||||
|
b.Navigation("OwnerNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "UserNavigation")
|
||||||
|
.WithMany("Sessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("user");
|
||||||
|
|
||||||
|
b.Navigation("UserNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bots");
|
||||||
|
|
||||||
|
b.Navigation("Sessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UseUserIdAsForeignKey : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "sessions_ibfk_1",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "bots_ibfk_1",
|
||||||
|
table: "bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "username",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots"
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
table: "bots",
|
||||||
|
name: "owner",
|
||||||
|
newName: "owner_tmp");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<uint>(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions",
|
||||||
|
type: "int(10) unsigned",
|
||||||
|
nullable: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<uint>(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots",
|
||||||
|
type: "int(10) unsigned",
|
||||||
|
nullable: false);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions",
|
||||||
|
column: "user");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots",
|
||||||
|
column: "owner");
|
||||||
|
|
||||||
|
// Migrate data
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
UPDATE sessions INNER JOIN users ON sessions.username=users.username
|
||||||
|
SET sessions.user = users.id;
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
UPDATE bots INNER JOIN users ON bots.owner_tmp=users.username
|
||||||
|
SET bots.owner = users.id;
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots",
|
||||||
|
column: "owner",
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions",
|
||||||
|
column: "user",
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "username",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "owner_tmp",
|
||||||
|
table: "bots");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "user",
|
||||||
|
table: "sessions");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "username1",
|
||||||
|
table: "users",
|
||||||
|
newName: "username2");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "username",
|
||||||
|
table: "sessions",
|
||||||
|
type: "varchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
collation: "utf8mb4_unicode_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "owner",
|
||||||
|
table: "bots",
|
||||||
|
type: "varchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_unicode_ci",
|
||||||
|
oldClrType: typeof(uint),
|
||||||
|
oldType: "int(10) unsigned")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_users_username",
|
||||||
|
table: "users",
|
||||||
|
column: "username");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "username1",
|
||||||
|
table: "sessions",
|
||||||
|
column: "username");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "bots_ibfk_1",
|
||||||
|
table: "bots",
|
||||||
|
column: "owner",
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "username",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "sessions_ibfk_1",
|
||||||
|
table: "sessions",
|
||||||
|
column: "username",
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "username",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CollabVMAuthDbContext))]
|
||||||
|
partial class CollabVMAuthDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.UseCollation("utf8mb4_unicode_ci")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.15")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "utf8mb4");
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<uint>("Owner")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("owner");
|
||||||
|
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("char(64)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Owner" }, "owner");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Token" }, "token")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("bots", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.IpBan", b =>
|
||||||
|
{
|
||||||
|
b.Property<byte[]>("Ip")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("BannedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("banned_at")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<string>("BannedBy")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("banned_by");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("reason");
|
||||||
|
|
||||||
|
b.HasKey("Ip")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.ToTable("ip_bans", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("char(32)")
|
||||||
|
.HasColumnName("token")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<byte[]>("LastIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("last_ip");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUsed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("last_used")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("UserId")
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("user");
|
||||||
|
|
||||||
|
b.HasKey("Token")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "UserId" }, "user");
|
||||||
|
|
||||||
|
b.ToTable("sessions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<uint>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<uint>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BanReason")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("ban_reason");
|
||||||
|
|
||||||
|
b.Property<bool>("Banned")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("banned");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp")
|
||||||
|
.HasColumnName("created")
|
||||||
|
.HasDefaultValueSql("current_timestamp()");
|
||||||
|
|
||||||
|
b.Property<uint>("CvmRank")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int(10) unsigned")
|
||||||
|
.HasColumnName("cvm_rank")
|
||||||
|
.HasDefaultValueSql("'1'");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("DateOfBirth")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date_of_birth");
|
||||||
|
|
||||||
|
b.Property<bool>("Developer")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("developer");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("email");
|
||||||
|
|
||||||
|
b.Property<string>("EmailVerificationCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("email_verification_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<bool>("EmailVerified")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasColumnName("email_verified");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("password");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordResetCode")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("char(8)")
|
||||||
|
.HasColumnName("password_reset_code")
|
||||||
|
.IsFixedLength();
|
||||||
|
|
||||||
|
b.Property<byte[]>("RegistrationIp")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varbinary(16)")
|
||||||
|
.HasColumnName("registration_ip");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)")
|
||||||
|
.HasColumnName("username");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Email" }, "email")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Username" }, "username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("username1");
|
||||||
|
|
||||||
|
b.ToTable("users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Bot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "OwnerNavigation")
|
||||||
|
.WithMany("Bots")
|
||||||
|
.HasForeignKey("Owner")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("bots_ibfk_1");
|
||||||
|
|
||||||
|
b.Navigation("OwnerNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.Session", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Computernewb.CollabVMAuthServer.Database.Schema.User", "UserNavigation")
|
||||||
|
.WithMany("Sessions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("sessions_ibfk_1");
|
||||||
|
|
||||||
|
b.Navigation("UserNavigation");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Computernewb.CollabVMAuthServer.Database.Schema.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Bots");
|
||||||
|
|
||||||
|
b.Navigation("Sessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
CollabVMAuthServer/Database/Schema/Bot.cs
Normal file
20
CollabVMAuthServer/Database/Schema/Bot.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
|
||||||
|
public partial class Bot
|
||||||
|
{
|
||||||
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Token { get; set; } = null!;
|
||||||
|
|
||||||
|
public uint CvmRank { get; set; }
|
||||||
|
|
||||||
|
public uint Owner { get; set; }
|
||||||
|
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
|
public virtual User OwnerNavigation { get; set; } = null!;
|
||||||
|
}
|
||||||
15
CollabVMAuthServer/Database/Schema/IpBan.cs
Normal file
15
CollabVMAuthServer/Database/Schema/IpBan.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
|
||||||
|
public partial class IpBan
|
||||||
|
{
|
||||||
|
public byte[] Ip { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Reason { get; set; } = null!;
|
||||||
|
|
||||||
|
public string? BannedBy { get; set; }
|
||||||
|
|
||||||
|
public DateTime BannedAt { get; set; }
|
||||||
|
}
|
||||||
7
CollabVMAuthServer/Database/Schema/Rank.cs
Normal file
7
CollabVMAuthServer/Database/Schema/Rank.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
|
||||||
|
public enum Rank : uint {
|
||||||
|
Registered = 1,
|
||||||
|
Admin = 2,
|
||||||
|
Moderator = 3
|
||||||
|
}
|
||||||
19
CollabVMAuthServer/Database/Schema/Session.cs
Normal file
19
CollabVMAuthServer/Database/Schema/Session.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
|
||||||
|
public partial class Session
|
||||||
|
{
|
||||||
|
public string Token { get; set; } = null!;
|
||||||
|
|
||||||
|
public uint UserId { get; set; }
|
||||||
|
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
|
public DateTime LastUsed { get; set; }
|
||||||
|
|
||||||
|
public byte[] LastIp { get; set; } = null!;
|
||||||
|
|
||||||
|
public virtual User UserNavigation { get; set; } = null!;
|
||||||
|
}
|
||||||
39
CollabVMAuthServer/Database/Schema/User.cs
Normal file
39
CollabVMAuthServer/Database/Schema/User.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
|
||||||
|
public partial class User
|
||||||
|
{
|
||||||
|
public uint Id { get; set; }
|
||||||
|
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Password { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Email { get; set; } = null!;
|
||||||
|
|
||||||
|
public DateOnly DateOfBirth { get; set; }
|
||||||
|
|
||||||
|
public bool EmailVerified { get; set; }
|
||||||
|
|
||||||
|
public string? EmailVerificationCode { get; set; }
|
||||||
|
|
||||||
|
public string? PasswordResetCode { get; set; }
|
||||||
|
|
||||||
|
public uint CvmRank { get; set; }
|
||||||
|
|
||||||
|
public bool Banned { get; set; }
|
||||||
|
|
||||||
|
public string? BanReason { get; set; }
|
||||||
|
|
||||||
|
public byte[] RegistrationIp { get; set; } = null!;
|
||||||
|
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
|
||||||
|
public bool Developer { get; set; }
|
||||||
|
|
||||||
|
public virtual ICollection<Bot> Bots { get; set; } = new List<Bot>();
|
||||||
|
|
||||||
|
public virtual ICollection<Session> Sessions { get; set; } = new List<Session>();
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|
||||||
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
|
|
||||||
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);
|
|
||||||
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 user = await Utilities.GetStaffByToken(payload.session);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new IPBanResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid session"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
// Check rank
|
|
||||||
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 user = await Utilities.GetStaffByToken(payload.token);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new IPBanResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid session"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
// Check rank
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
// 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 user = await Utilities.GetStaffByToken(payload.token);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new IPBanResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid session"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
// Check rank
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
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 user = await Utilities.GetStaffByToken(payload.token);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new IPBanResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid session"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
// Check rank
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
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 user = await Utilities.GetStaffByToken(payload.token);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new IPBanResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid session"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
// Check rank
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// 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("[", "![") + "%";
|
|
||||||
}
|
|
||||||
IPAddress? filterIp = null;
|
|
||||||
if (payload.filterIp != null)
|
|
||||||
{
|
|
||||||
if (!IPAddress.TryParse(payload.filterIp, out filterIp)) {
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return Results.Json(new AdminUsersResponse
|
|
||||||
{
|
|
||||||
success = false,
|
|
||||||
error = "Invalid filterIp"
|
|
||||||
}, Utilities.JsonSerializerOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var users = (await Program.Database.ListUsers(filterUsername, filterIp, 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,
|
|
||||||
banReason = user.BanReason ?? "",
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
149
CollabVMAuthServer/HTTP/CollabVMAuthenticationHandler.cs
Normal file
149
CollabVMAuthServer/HTTP/CollabVMAuthenticationHandler.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP;
|
||||||
|
|
||||||
|
public partial class CollabVMAuthenticationHandler : SignInAuthenticationHandler<CollabVMAuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
public CollabVMAuthenticationHandler(IOptionsMonitor<CollabVMAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[GeneratedRegex("^Session (?<token>.+)$")]
|
||||||
|
private static partial Regex AuthorizationHeaderRegex();
|
||||||
|
|
||||||
|
private string? GetSessionTokenFromAuthorizationHeader() {
|
||||||
|
// Check for Authorization header
|
||||||
|
var authorizationHeader = Context.Request.Headers.Authorization.ToString();
|
||||||
|
if (AuthorizationHeaderRegex().Match(authorizationHeader).Groups.TryGetValue("token", out var match)) {
|
||||||
|
return match.Value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetSessionTokenFromCookie() {
|
||||||
|
if (Context.Request.Cookies.TryGetValue("collabvm_session", out var token)) {
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetSessionTokenFromBody() {
|
||||||
|
// This is how the current webapp sends the token.
|
||||||
|
// I really do not like this. Should be changed to use authorization or cookie and then we can eventually axe this
|
||||||
|
if (Context.Request.ContentType != "application/json") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Context.Request.EnableBuffering();
|
||||||
|
var payload = await Context.Request.ReadFromJsonAsync<RequestBodyAuthenticationPayload>();
|
||||||
|
// sigh
|
||||||
|
Context.Request.Body.Position = 0;
|
||||||
|
// This can be two different keys because I was on crack cocaine when I wrote the original API
|
||||||
|
return
|
||||||
|
payload?.Session ??
|
||||||
|
payload?.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
// There are multiple ways the client can send a session token. We check them in order of preference
|
||||||
|
var sessionToken =
|
||||||
|
GetSessionTokenFromAuthorizationHeader() ??
|
||||||
|
GetSessionTokenFromCookie() ??
|
||||||
|
await GetSessionTokenFromBody();
|
||||||
|
|
||||||
|
// If no session token was provided, fail
|
||||||
|
if (sessionToken == null) {
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open db and find session
|
||||||
|
using var dbContext = new CollabVMAuthDbContext(Options.DbContextOptions);
|
||||||
|
Claim[] claims = [];
|
||||||
|
|
||||||
|
if (sessionToken.Length == 32) { // User
|
||||||
|
var session = await dbContext.Sessions.Include(s => s.UserNavigation).FirstOrDefaultAsync(s => s.Token == sessionToken);
|
||||||
|
|
||||||
|
// Fail if invalid token or expired
|
||||||
|
if (session == null || DateTime.UtcNow > session.LastUsed.AddDays(Program.Config.Accounts!.SessionExpiryDays)) {
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
claims = [
|
||||||
|
new("type", "user"),
|
||||||
|
new("id", session.UserNavigation.Id.ToString()),
|
||||||
|
new("username", session.UserNavigation.Username),
|
||||||
|
new("rank", session.UserNavigation.CvmRank.ToString()),
|
||||||
|
new("developer", session.UserNavigation.Developer ? "1" : "0")
|
||||||
|
];
|
||||||
|
} else if (sessionToken.Length == 64) { // Bot
|
||||||
|
var bot = await dbContext.Bots.FirstOrDefaultAsync(b => b.Token == sessionToken);
|
||||||
|
|
||||||
|
// Fail if unknown bot
|
||||||
|
if (bot == null) {
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
claims = [
|
||||||
|
new("type", "bot"),
|
||||||
|
new("id", bot.Id.ToString()),
|
||||||
|
new("username", bot.Username),
|
||||||
|
new("rank", bot.CvmRank.ToString())
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success result
|
||||||
|
return AuthenticateResult.Success(
|
||||||
|
new AuthenticationTicket(
|
||||||
|
new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(claims, Scheme.Name)
|
||||||
|
),
|
||||||
|
Scheme.Name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
|
||||||
|
{
|
||||||
|
var token = Utilities.RandomString(32);
|
||||||
|
using var dbContext = new CollabVMAuthDbContext(Options.DbContextOptions);
|
||||||
|
// Add to database
|
||||||
|
await dbContext.Sessions.AddAsync(new Session {
|
||||||
|
Token = token,
|
||||||
|
UserId = uint.Parse(user.FindFirstValue("id")
|
||||||
|
?? throw new InvalidOperationException("User ID claim was null")),
|
||||||
|
Created = DateTime.UtcNow,
|
||||||
|
LastUsed = DateTime.UtcNow,
|
||||||
|
LastIp = Context.Connection.RemoteIpAddress!.GetAddressBytes(),
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
// Add claim
|
||||||
|
user.Identities.First().AddClaim(new("token", token));
|
||||||
|
// Set cookie
|
||||||
|
Context.Response.Cookies.Append("collabvm_session", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task HandleSignOutAsync(AuthenticationProperties? properties)
|
||||||
|
{
|
||||||
|
throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP;
|
||||||
|
|
||||||
|
public class CollabVMAuthenticationSchemeOptions : AuthenticationSchemeOptions {
|
||||||
|
public DbContextOptions<CollabVMAuthDbContext> DbContextOptions { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Policy;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP;
|
||||||
|
|
||||||
|
public class CollabVMAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
|
||||||
|
{
|
||||||
|
private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();
|
||||||
|
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
|
||||||
|
{
|
||||||
|
if (authorizeResult.Forbidden) {
|
||||||
|
context.Response.StatusCode = 403;
|
||||||
|
var requirement = authorizeResult.AuthorizationFailure!.FailedRequirements.First();
|
||||||
|
if (requirement is ClaimsAuthorizationRequirement req) {
|
||||||
|
if (req.ClaimType == "rank") {
|
||||||
|
await context.Response.WriteAsJsonAsync(new ApiResponse {
|
||||||
|
success = false,
|
||||||
|
error = "You do not have the correct rank to do that."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (req.ClaimType == "developer") {
|
||||||
|
await context.Response.WriteAsJsonAsync(new ApiResponse {
|
||||||
|
success = false,
|
||||||
|
error = "You must be a developer to do that."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await context.Response.WriteAsJsonAsync(new ApiResponse {
|
||||||
|
success = false,
|
||||||
|
error = "Access forbidden."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (authorizeResult.Challenged) {
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
await context.Response.WriteAsJsonAsync(new ApiResponse {
|
||||||
|
success = false,
|
||||||
|
error = "You need to login to do that."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default handler
|
||||||
|
await defaultHandler.HandleAsync(next, context, policy, authorizeResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
CollabVMAuthServer/HTTP/Controllers/AdminApiController.cs
Normal file
308
CollabVMAuthServer/HTTP/Controllers/AdminApiController.cs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP.Controllers;
|
||||||
|
|
||||||
|
[Route("api/v1/admin")]
|
||||||
|
[ApiController]
|
||||||
|
public class AdminApiController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CollabVMAuthDbContext _dbContext;
|
||||||
|
public AdminApiController(CollabVMAuthDbContext dbContext) {
|
||||||
|
this._dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("ipban")]
|
||||||
|
[Authorize("Staff")]
|
||||||
|
public async Task<IResult> HandleIPBan(IPBanPayload payload)
|
||||||
|
{
|
||||||
|
var ip = IPAddress.Parse(payload.ip).GetAddressBytes();
|
||||||
|
// Find or create ban
|
||||||
|
var ban = await _dbContext.IpBans.FirstOrDefaultAsync(b => b.Ip == ip);
|
||||||
|
|
||||||
|
if (payload.banned)
|
||||||
|
{
|
||||||
|
ban ??= new IpBan { Ip = ip };
|
||||||
|
ban.Reason = payload.reason;
|
||||||
|
ban.BannedBy = HttpContext.User.FindFirstValue("username");
|
||||||
|
ban.BannedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ban == null) {
|
||||||
|
return Results.Json(new ApiResponse {
|
||||||
|
success = false,
|
||||||
|
error = "IP is not banned."
|
||||||
|
}, statusCode: 400);
|
||||||
|
}
|
||||||
|
_dbContext.IpBans.Remove(ban);
|
||||||
|
}
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("ban")]
|
||||||
|
[Authorize("Staff")]
|
||||||
|
public async Task<IResult> HandleBanUser(BanUserPayload payload)
|
||||||
|
{
|
||||||
|
// Check target user
|
||||||
|
var targetUser = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (targetUser == null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "User not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Set ban
|
||||||
|
targetUser.Banned = payload.banned;
|
||||||
|
targetUser.BanReason = payload.reason;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("updatebot")]
|
||||||
|
[Authorize("Staff")]
|
||||||
|
public async Task<IResult> HandleAdminUpdateBot(AdminUpdateBotPayload payload)
|
||||||
|
{
|
||||||
|
// Check target bot
|
||||||
|
var targetBot = await _dbContext.Bots.FirstOrDefaultAsync(b => b.Username == payload.username);
|
||||||
|
if (targetBot == null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Bot not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Make sure at least one field is being updated
|
||||||
|
if (payload.rank == null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "No fields to update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Moderators cannot promote bots to admin, and can only promote their own bots to moderator
|
||||||
|
if ((Rank)payload.rank == Rank.Admin && HttpContext.User.FindFirstValue("rank") == "3")
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Insufficient permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetBot.Owner != uint.Parse(HttpContext.User.FindFirstValue("id")!) && HttpContext.User.FindFirstValue("rank") == "3")
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Insufficient permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check rank
|
||||||
|
uint? rank = payload.rank;
|
||||||
|
if (rank != null) {
|
||||||
|
if (rank < 1 || rank > 3) {
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid rank"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
targetBot.CvmRank = payload.rank.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("updateuser")]
|
||||||
|
[Authorize("Staff")]
|
||||||
|
public async Task<IResult> HandleAdminUpdateUser(AdminUpdateUserPayload payload)
|
||||||
|
{
|
||||||
|
// Check target user
|
||||||
|
var targetUser = await _dbContext.Users.Include(u => u.Bots).FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (targetUser == null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "User not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check rank
|
||||||
|
uint? rank = payload.rank;
|
||||||
|
if (rank != null) {
|
||||||
|
if (rank < 1 || rank > 3) {
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid rank"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moderators cannot change ranks
|
||||||
|
if (HttpContext.User.FindFirstValue("rank") == "3") {
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Insufficient permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser.CvmRank = rank.Value;
|
||||||
|
}
|
||||||
|
// Check developer
|
||||||
|
if (payload.developer != null) {
|
||||||
|
targetUser.Developer = payload.developer.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser.Developer == false) {
|
||||||
|
targetUser.Bots.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("users")]
|
||||||
|
[Authorize("Staff")]
|
||||||
|
public async Task<IResult> HandleAdminUsers(AdminUsersPayload payload)
|
||||||
|
{
|
||||||
|
// Validate orderBy
|
||||||
|
if (payload.orderBy != null && !new string[] { "id", "username", "email", "date_of_birth", "cvm_rank", "banned", "created" }.Contains(payload.orderBy))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new AdminUsersResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid orderBy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Filter IP
|
||||||
|
IPAddress? filterIp = null;
|
||||||
|
if (payload.filterIp != null)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(payload.filterIp, out filterIp)) {
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new AdminUsersResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid filterIp"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users
|
||||||
|
IQueryable<User> result = _dbContext.Users;
|
||||||
|
|
||||||
|
if (payload.filterUsername != null) {
|
||||||
|
result = result.Where(u => u.Username.Contains(payload.filterUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterIp != null) {
|
||||||
|
result = result.Where(u => u.RegistrationIp == filterIp.GetAddressBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderBy = payload.orderBy ?? "id";
|
||||||
|
var order = (Expression<Func<User, object>> k) => {
|
||||||
|
if (payload.orderByDescending) {
|
||||||
|
result = result.OrderByDescending(k);
|
||||||
|
} else {
|
||||||
|
result = result.OrderBy(k);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
switch (orderBy) {
|
||||||
|
case "id":
|
||||||
|
order(u => u.Id);
|
||||||
|
break;
|
||||||
|
case "username":
|
||||||
|
order(u => u.Username);
|
||||||
|
break;
|
||||||
|
case "email":
|
||||||
|
order(u => u.Email);
|
||||||
|
break;
|
||||||
|
case "date_of_birth":
|
||||||
|
order(u => u.DateOfBirth);
|
||||||
|
break;
|
||||||
|
case "cvm_rank":
|
||||||
|
order(u => u.CvmRank);
|
||||||
|
break;
|
||||||
|
case "banned":
|
||||||
|
order(u => u.Banned);
|
||||||
|
break;
|
||||||
|
case "created":
|
||||||
|
order(u => u.Created);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.Skip((payload.page - 1) * payload.resultsPerPage).Take(payload.resultsPerPage);
|
||||||
|
|
||||||
|
var users = await result.Select(user => new AdminUser
|
||||||
|
{
|
||||||
|
id = user.Id,
|
||||||
|
username = user.Username,
|
||||||
|
email = user.Email,
|
||||||
|
rank = user.CvmRank,
|
||||||
|
banned = user.Banned,
|
||||||
|
banReason = user.BanReason ?? "",
|
||||||
|
dateOfBirth = user.DateOfBirth.ToString("yyyy-MM-dd"),
|
||||||
|
dateJoined = user.Created.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||||
|
registrationIp = new IPAddress(user.RegistrationIp).ToString(),
|
||||||
|
developer = user.Developer
|
||||||
|
}).ToArrayAsync();
|
||||||
|
|
||||||
|
return Results.Json(new AdminUsersResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
users = users,
|
||||||
|
totalPageCount = (int)Math.Ceiling(await _dbContext.Users.CountAsync() / (double)payload.resultsPerPage)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,754 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
using Isopoh.Cryptography.Argon2;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP.Controllers;
|
||||||
|
|
||||||
|
[Route("api/v1")]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthenticationApiController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CollabVMAuthDbContext _dbContext;
|
||||||
|
public AuthenticationApiController(CollabVMAuthDbContext dbContext) {
|
||||||
|
this._dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("sendreset")]
|
||||||
|
public async Task<IResult> HandleSendReset(SendResetEmailPayload payload)
|
||||||
|
{
|
||||||
|
if (!Program.Config.SMTP!.Enabled)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Password reset is not supported by this server. Please contact an administrator."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check captcha response
|
||||||
|
if (Program.Config.hCaptcha!.Enabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload.captchaToken))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Missing hCaptcha token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var result =
|
||||||
|
await Program.hCaptcha!.Verify(payload.captchaToken, HttpContext.Connection.RemoteIpAddress!.ToString());
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid captcha response"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check username and E-Mail
|
||||||
|
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (user == null || user.Email != payload.email)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid username or E-Mail"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Generate reset code
|
||||||
|
var code = Program.Random.Next(10000000, 99999999).ToString();
|
||||||
|
user.PasswordResetCode = code;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
await Program.Mailer!.SendPasswordResetEmail(payload.username, payload.email, code);
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("reset")]
|
||||||
|
public async Task<IResult> HandleReset(ResetPasswordPayload payload)
|
||||||
|
{
|
||||||
|
// Is mailer enabled?
|
||||||
|
if (Program.Mailer == null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Password reset is disabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check username and E-Mail
|
||||||
|
var user = await _dbContext.Users.Include(u => u.Sessions).FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (user == null || user.Email != payload.email)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid username or E-Mail"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if code is correct
|
||||||
|
if (user.PasswordResetCode != payload.code)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid reset code"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate new password
|
||||||
|
if (!Utilities.ValidatePassword(payload.newPassword))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Passwords must be at least 8 characters and must contain an uppercase and lowercase letter, a number, and a symbol."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Program.BannedPasswords.Contains(payload.newPassword))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That password is commonly used and is not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Reset password
|
||||||
|
var newPasswordHashed = Argon2.Hash(payload.newPassword);
|
||||||
|
user.Password = newPasswordHashed;
|
||||||
|
user.PasswordResetCode = null;
|
||||||
|
user.Sessions.Clear();
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("update")]
|
||||||
|
[Authorize("User")]
|
||||||
|
public async Task<IResult> HandleUpdate(UpdatePayload payload)
|
||||||
|
{
|
||||||
|
var user = await _dbContext.Users.Include(u => u.Sessions).FirstOrDefaultAsync(u => u.Id == uint.Parse(HttpContext.User.FindFirstValue("id")!));
|
||||||
|
// Check password
|
||||||
|
if (!Argon2.Verify(user!.Password, payload.currentPassword))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate new username
|
||||||
|
if (payload.username != null)
|
||||||
|
{
|
||||||
|
if (!Utilities.ValidateUsername(payload.username))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and must be between 3 and 20 characters."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Make sure username isn't taken
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Username == payload.username) || await _dbContext.Bots.AnyAsync(b => b.Username == payload.username))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That username is taken."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.Username = payload.username;
|
||||||
|
}
|
||||||
|
// Validate new E-Mail
|
||||||
|
if (payload.email != null)
|
||||||
|
{
|
||||||
|
if (!new EmailAddressAttribute().IsValid(payload.email))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Malformed E-Mail address."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Program.Config.Registration!.EmailDomainWhitelist && !Program.Config.Registration!.AllowedEmailDomains!.Contains(payload.email.Split("@")[1]))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That E-Mail domain is not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if E-Mail is in use
|
||||||
|
if (_dbContext.Users.Any(u => u.Email == payload.email))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That E-Mail is already in use."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.Email = payload.email;
|
||||||
|
}
|
||||||
|
// Validate new password
|
||||||
|
if (payload.newPassword != null)
|
||||||
|
{
|
||||||
|
if (!Utilities.ValidatePassword(payload.newPassword))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Passwords must be at least 8 characters and must contain an uppercase and lowercase letter, a number, and a symbol."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Program.BannedPasswords.Contains(payload.newPassword))
|
||||||
|
{
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That password is commonly used and is not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.Password = Argon2.Hash(payload.newPassword);
|
||||||
|
}
|
||||||
|
// Revoke all sessions
|
||||||
|
user.Sessions.Clear();
|
||||||
|
// Unverify the account if the E-Mail was changed
|
||||||
|
if (payload.email != null && Program.Config.Registration!.EmailVerificationRequired)
|
||||||
|
{
|
||||||
|
user.EmailVerified = false;
|
||||||
|
user.EmailVerificationCode = Program.Random.Next(10000000, 99999999).ToString();
|
||||||
|
await Program.Mailer!.SendVerificationCode(user.Username, payload.email, user.EmailVerificationCode);
|
||||||
|
}
|
||||||
|
// Save changes
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new UpdateResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
verificationRequired = !user.EmailVerified,
|
||||||
|
sessionExpired = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("logout")]
|
||||||
|
[Authorize("User")]
|
||||||
|
public async Task<IResult> HandleLogout()
|
||||||
|
{
|
||||||
|
var user = await _dbContext.Users.Include(u => u.Sessions).FirstOrDefaultAsync(u => u.Id == uint.Parse(HttpContext.User.FindFirstValue("id")!));
|
||||||
|
// Revoke session
|
||||||
|
user!.Sessions.Clear();
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Json(new ApiResponse
|
||||||
|
{
|
||||||
|
success = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("session")]
|
||||||
|
[Authorize("User")]
|
||||||
|
public async Task<IResult> HandleSession()
|
||||||
|
{
|
||||||
|
var user = await _dbContext.Users.FindAsync(uint.Parse(HttpContext.User.FindFirstValue("id")!));
|
||||||
|
return Results.Json(new SessionResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
banned = user!.Banned,
|
||||||
|
username = user.Username,
|
||||||
|
email = user.Email,
|
||||||
|
rank = user.CvmRank,
|
||||||
|
developer = user.Developer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("join")]
|
||||||
|
public async Task<IResult> HandleJoin(JoinPayload payload)
|
||||||
|
{
|
||||||
|
// Check secret key
|
||||||
|
if (payload.secretKey != Program.Config.CollabVM!.SecretKey)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 401;
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid secret key"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if IP banned
|
||||||
|
if (!IPAddress.TryParse(payload.ip, out var ip))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Malformed IP address"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var ban = await _dbContext.IpBans.FirstOrDefaultAsync(b => b.Ip == ip.GetAddressBytes());
|
||||||
|
if (ban != null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 200;
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = false,
|
||||||
|
error = "Banned",
|
||||||
|
banned = true,
|
||||||
|
banReason = ban.Reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if session is valid
|
||||||
|
if (payload.sessionToken.Length == 32)
|
||||||
|
{
|
||||||
|
// User
|
||||||
|
var session = await _dbContext.Sessions.Include(s => s.UserNavigation).FirstOrDefaultAsync(s => s.Token == payload.sessionToken);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = false,
|
||||||
|
error = "Invalid session",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if banned
|
||||||
|
if (session.UserNavigation.Banned)
|
||||||
|
{
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = false,
|
||||||
|
banned = true,
|
||||||
|
error = "Banned",
|
||||||
|
banReason = session.UserNavigation.BanReason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Update session
|
||||||
|
session.LastUsed = DateTime.UtcNow;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = true,
|
||||||
|
username = session.UserNavigation.Username,
|
||||||
|
rank = session.UserNavigation.CvmRank
|
||||||
|
});
|
||||||
|
} else if (payload.sessionToken.Length == 64)
|
||||||
|
{
|
||||||
|
// Bot
|
||||||
|
var bot = await _dbContext.Bots.FirstOrDefaultAsync(b => b.Token == payload.sessionToken);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = false,
|
||||||
|
error = "Invalid session",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = true,
|
||||||
|
username = bot.Username,
|
||||||
|
rank = bot.CvmRank
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new JoinResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
clientSuccess = false,
|
||||||
|
error = "Invalid session"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("login")]
|
||||||
|
public async Task<IResult> HandleLogin(LoginPayload payload)
|
||||||
|
{
|
||||||
|
// Check captcha response
|
||||||
|
if (Program.Config.hCaptcha!.Enabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload.captchaToken))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Missing hCaptcha token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var result =
|
||||||
|
await Program.hCaptcha!.Verify(payload.captchaToken, HttpContext.Connection.RemoteIpAddress!.ToString());
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid captcha response"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate username and password
|
||||||
|
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid username or password"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if IP banned
|
||||||
|
var ban = await _dbContext.IpBans.FirstOrDefaultAsync(b => b.Ip == HttpContext.Connection.RemoteIpAddress!.GetAddressBytes());
|
||||||
|
if (ban != null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = $"You are banned: {ban.Reason}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if account is verified
|
||||||
|
if (!user.EmailVerified && Program.Config.Registration!.EmailVerificationRequired)
|
||||||
|
{
|
||||||
|
if (user.EmailVerificationCode == null) {
|
||||||
|
user.EmailVerificationCode = Program.Random.Next(10000000, 99999999).ToString();
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
await Program.Mailer!.SendVerificationCode(user.Username, user.Email, user.EmailVerificationCode);
|
||||||
|
}
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
verificationRequired = true,
|
||||||
|
email = user.Email,
|
||||||
|
username = user.Username,
|
||||||
|
rank = user.CvmRank,
|
||||||
|
developer = user.Developer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check max sessions
|
||||||
|
var sessions = await _dbContext.Sessions.Include(s => s.UserNavigation).CountAsync(s => s.UserNavigation.Username == user.Username);
|
||||||
|
if (sessions >= Program.Config.Accounts!.MaxSessions)
|
||||||
|
{
|
||||||
|
var oldest = await _dbContext.Sessions.Include(s => s.UserNavigation).Where(s => s.UserNavigation.Username == user.Username).OrderBy(s => s.LastUsed).FirstAsync();
|
||||||
|
_dbContext.Sessions.Remove(oldest);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
// Perform sign-in
|
||||||
|
var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity([new("id", user.Id.ToString())]));
|
||||||
|
await HttpContext.SignInAsync(userPrincipal);
|
||||||
|
var token = userPrincipal.FindFirstValue("token")
|
||||||
|
?? throw new InvalidOperationException("Sign in handler did not add token");
|
||||||
|
return Results.Json(new LoginResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
token = token,
|
||||||
|
username = user.Username,
|
||||||
|
email = user.Email,
|
||||||
|
rank = user.CvmRank,
|
||||||
|
developer = user.Developer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("verify")]
|
||||||
|
public async Task<IResult> HandleVerify(VerifyPayload payload)
|
||||||
|
{
|
||||||
|
// Validate username and password
|
||||||
|
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == payload.username);
|
||||||
|
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new VerifyResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid username or password"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if account is verified
|
||||||
|
if (user.EmailVerified)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new VerifyResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Account is already verified"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if code is correct
|
||||||
|
if (user.EmailVerificationCode != payload.code)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new VerifyResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid verification code"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Verify the account
|
||||||
|
user.EmailVerified = true;
|
||||||
|
// Create a session
|
||||||
|
var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity([new("id", user.Id.ToString())]));
|
||||||
|
await HttpContext.SignInAsync(userPrincipal);
|
||||||
|
var token = userPrincipal.FindFirstValue("token")
|
||||||
|
?? throw new InvalidOperationException("Sign in handler did not add token");
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new VerifyResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
sessionToken = token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("register")]
|
||||||
|
public async Task<IResult> HandleRegister(RegisterPayload payload)
|
||||||
|
{
|
||||||
|
// Check if IP banned
|
||||||
|
var ban = await _dbContext.IpBans.FirstOrDefaultAsync(b => b.Ip == HttpContext.Connection.RemoteIpAddress!.GetAddressBytes());
|
||||||
|
if (ban != null)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = $"You are banned: {ban.Reason}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check captcha response
|
||||||
|
if (Program.Config.hCaptcha!.Enabled)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload.captchaToken))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Missing hCaptcha token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var result =
|
||||||
|
await Program.hCaptcha!.Verify(payload.captchaToken, HttpContext.Connection.RemoteIpAddress!.ToString());
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid captcha response"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make sure username isn't taken
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Username == payload.username) || await _dbContext.Bots.AnyAsync(b => b.Username == payload.username))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That username is taken."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Check if E-Mail is in use
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Email == payload.email))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That E-Mail is already in use."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate username
|
||||||
|
if (!Utilities.ValidateUsername(payload.username))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and must be between 3 and 20 characters."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate E-Mail
|
||||||
|
if (!new EmailAddressAttribute().IsValid(payload.email))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Malformed E-Mail address."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Program.Config.Registration!.EmailDomainWhitelist &&
|
||||||
|
!Program.Config.Registration.AllowedEmailDomains!.Contains(payload.email.Split("@")[1]))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That E-Mail domain is not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate password
|
||||||
|
if (!Utilities.ValidatePassword(payload.password))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Passwords must be at least 8 characters and must contain an uppercase and lowercase letter, a number, and a symbol."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Program.BannedPasswords.Contains(payload.password))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That password is commonly used and is not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Validate date of birth
|
||||||
|
if (!DateOnly.TryParseExact(payload.dateOfBirth, "yyyy-MM-dd", out var dob))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Invalid date of birth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dob.AddYears(13) > DateOnly.FromDateTime(DateTime.Now))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
await _dbContext.IpBans.AddAsync(new IpBan {
|
||||||
|
Ip = HttpContext.Connection.RemoteIpAddress!.GetAddressBytes(),
|
||||||
|
Reason = "You are not old enough to use CollabVM.",
|
||||||
|
BannedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "You are not old enough to use CollabVM."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// theres no fucking chance
|
||||||
|
if (dob < new DateOnly(1954, 1, 1))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Are you sure about that?"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Create the account
|
||||||
|
string? token = null;
|
||||||
|
var user = new User {
|
||||||
|
Username = payload.username,
|
||||||
|
Password = Argon2.Hash(payload.password),
|
||||||
|
Email = payload.email,
|
||||||
|
DateOfBirth = dob,
|
||||||
|
// If this is the first user, make them an admin
|
||||||
|
CvmRank = (uint) ((await _dbContext.Users.AnyAsync()) ? 1 : 2),
|
||||||
|
RegistrationIp = HttpContext.Connection.RemoteIpAddress!.GetAddressBytes(),
|
||||||
|
Created = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
_dbContext.Users.Add(user);
|
||||||
|
|
||||||
|
if (Program.Config.Registration.EmailVerificationRequired)
|
||||||
|
{
|
||||||
|
user.EmailVerificationCode = Program.Random.Next(10000000, 99999999).ToString();
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
await Program.Mailer!.SendVerificationCode(user.Username, user.Email, user.EmailVerificationCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.EmailVerified = true;
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity([new("id", user.Id.ToString())]));
|
||||||
|
await HttpContext.SignInAsync(userPrincipal);
|
||||||
|
token = userPrincipal.FindFirstValue("token")
|
||||||
|
?? throw new InvalidOperationException("Sign in handler did not add token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Json(new RegisterResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
verificationRequired = !user.EmailVerified,
|
||||||
|
email = user.Email,
|
||||||
|
username = user.Username,
|
||||||
|
sessionToken = token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("info")]
|
||||||
|
public IResult HandleInfo()
|
||||||
|
{
|
||||||
|
return Results.Json(new AuthServerInformation
|
||||||
|
{
|
||||||
|
// TODO: Implement registration closure
|
||||||
|
registrationOpen = true,
|
||||||
|
hcaptcha =
|
||||||
|
new() {
|
||||||
|
required = Program.Config.hCaptcha!.Enabled,
|
||||||
|
siteKey = Program.Config.hCaptcha.Enabled ? Program.Config.hCaptcha.SiteKey : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
115
CollabVMAuthServer/HTTP/Controllers/DeveloperApiController.cs
Normal file
115
CollabVMAuthServer/HTTP/Controllers/DeveloperApiController.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database.Schema;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||||
|
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP.Controllers;
|
||||||
|
|
||||||
|
[Route("api/v1/bots")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize("Developer")]
|
||||||
|
public class DeveloperApiController : ControllerBase {
|
||||||
|
|
||||||
|
private readonly CollabVMAuthDbContext _dbContext;
|
||||||
|
public DeveloperApiController(CollabVMAuthDbContext dbContext) {
|
||||||
|
this._dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("list")]
|
||||||
|
public async Task<IResult> HandleListBots(ListBotsPayload payload)
|
||||||
|
{
|
||||||
|
// owner can only be specified by admins and moderators
|
||||||
|
if (payload.owner != null && !(User.HasClaim("rank", "2") || User.HasClaim("rank", "3")))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 403;
|
||||||
|
return Results.Json(new ListBotsResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "Insufficient permissions"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Get bots
|
||||||
|
// If the user is not an admin, they can only see their own bots
|
||||||
|
IQueryable<Bot> result = _dbContext.Bots.Include(b => b.OwnerNavigation);
|
||||||
|
|
||||||
|
if (payload.owner != null) {
|
||||||
|
result = result.Where(b => b.OwnerNavigation.Username == payload.owner);
|
||||||
|
} else if (!User.HasClaim("rank", "2") && !User.HasClaim("rank", "3")) {
|
||||||
|
result = result.Where(b => b.OwnerNavigation.Username == User.FindFirstValue("username")!);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.Skip((payload.page - 1) * payload.resultsPerPage).Take(payload.resultsPerPage);
|
||||||
|
|
||||||
|
var bots = await result.Select(bot => new ListBot
|
||||||
|
{
|
||||||
|
id = (int)bot.Id,
|
||||||
|
username = bot.Username,
|
||||||
|
rank = bot.CvmRank,
|
||||||
|
owner = bot.OwnerNavigation.Username,
|
||||||
|
created = bot.Created.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
|
||||||
|
}).ToArrayAsync();
|
||||||
|
|
||||||
|
return Results.Json(new ListBotsResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
totalPageCount = (int)Math.Ceiling(await _dbContext.Bots.CountAsync() / (double)payload.resultsPerPage),
|
||||||
|
bots = bots
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("create")]
|
||||||
|
public async Task<IResult> HandleCreateBot(CreateBotPayload payload)
|
||||||
|
{
|
||||||
|
// Check bot username
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Username == payload.username) ||
|
||||||
|
await _dbContext.Bots.AnyAsync(b => b.Username == payload.username))
|
||||||
|
{
|
||||||
|
HttpContext.Response.StatusCode = 400;
|
||||||
|
return Results.Json(new CreateBotResponse
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = "That username is taken."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utilities.ValidateUsername(payload.username))
|
||||||
|
{
|
||||||
|
HttpContext.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."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Generate token
|
||||||
|
string token = Utilities.RandomString(64);
|
||||||
|
// Create bot
|
||||||
|
var bot = new Bot {
|
||||||
|
Username = payload.username,
|
||||||
|
Token = token,
|
||||||
|
CvmRank = 1,
|
||||||
|
Owner = uint.Parse(HttpContext.User.FindFirstValue("id")!),
|
||||||
|
Created = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_dbContext.Bots.Add(bot);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Results.Json(new CreateBotResponse
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
token = token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|
||||||
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
|
|
||||||
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 && user.Rank != Rank.Moderator)
|
|
||||||
{
|
|
||||||
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 and moderators
|
|
||||||
if (payload.owner != null && user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
|
|
||||||
{
|
|
||||||
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 || user.Rank == Rank.Moderator) ? 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class AdminUpdateBotPayload
|
public class AdminUpdateBotPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
public required string username { get; set; }
|
||||||
public string username { get; set; }
|
public uint? rank { get; set; }
|
||||||
public int? rank { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class AdminUpdateUserPayload
|
public class AdminUpdateUserPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
public required string username { get; set; }
|
||||||
public string username { get; set; }
|
public uint? rank { get; set; }
|
||||||
public int? rank { get; set; }
|
|
||||||
public bool? developer { get; set; } = null;
|
public bool? developer { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class AdminUsersPayload
|
public class AdminUsersPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
|
||||||
public int resultsPerPage { get; set; }
|
public int resultsPerPage { get; set; }
|
||||||
public int page { get; set; }
|
public int page { get; set; }
|
||||||
public string? filterUsername { get; set; }
|
public string? filterUsername { get; set; }
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class BanUserPayload
|
public class BanUserPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
public required string username { get; set; }
|
||||||
public string username { get; set; }
|
|
||||||
public bool banned { get; set; }
|
public bool banned { get; set; }
|
||||||
public string reason { get; set; }
|
public string? reason { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,5 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class CreateBotPayload
|
public class CreateBotPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
public required string username { get; set; }
|
||||||
public string username { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class IPBanPayload
|
public class IPBanPayload
|
||||||
{
|
{
|
||||||
public string session { get; set; }
|
public required string ip { get; set; }
|
||||||
public string ip { get; set; }
|
|
||||||
public bool banned { get; set; }
|
public bool banned { get; set; }
|
||||||
public string reason { get; set; }
|
public required string reason { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class JoinPayload
|
public class JoinPayload
|
||||||
{
|
{
|
||||||
public string secretKey { get; set; }
|
public required string secretKey { get; set; }
|
||||||
public string sessionToken { get; set; }
|
public required string sessionToken { get; set; }
|
||||||
public string ip { get; set; }
|
public required string ip { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class ListBotsPayload
|
public class ListBotsPayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
|
||||||
public int resultsPerPage { get; set; }
|
public int resultsPerPage { get; set; }
|
||||||
public int page { get; set; }
|
public int page { get; set; }
|
||||||
public string? owner { get; set; }
|
public string? owner { get; set; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class LoginPayload
|
public class LoginPayload
|
||||||
{
|
{
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string password { get; set; }
|
public required string password { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|
||||||
|
|
||||||
public class LogoutPayload
|
|
||||||
{
|
|
||||||
public string token { get; set; }
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class RegisterPayload
|
public class RegisterPayload
|
||||||
{
|
{
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string password { get; set; }
|
public required string password { get; set; }
|
||||||
public string email { get; set; }
|
public required string email { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
public string dateOfBirth { get; set; }
|
public required string dateOfBirth { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||||
|
|
||||||
|
public class RequestBodyAuthenticationPayload {
|
||||||
|
[JsonPropertyName("session")]
|
||||||
|
public string? Session { get; set; }
|
||||||
|
[JsonPropertyName("token")]
|
||||||
|
public string? Token { get; set; }
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class ResetPasswordPayload
|
public class ResetPasswordPayload
|
||||||
{
|
{
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string email { get; set; }
|
public required string email { get; set; }
|
||||||
public string code { get; set; }
|
public required string code { get; set; }
|
||||||
public string newPassword { get; set; }
|
public required string newPassword { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class SendResetEmailPayload
|
public class SendResetEmailPayload
|
||||||
{
|
{
|
||||||
public string email { get; set; }
|
public required string email { get; set; }
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string? captchaToken { get; set; }
|
public string? captchaToken { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|
||||||
|
|
||||||
public class SessionPayload
|
|
||||||
{
|
|
||||||
public string token { get; set; }
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class UpdatePayload
|
public class UpdatePayload
|
||||||
{
|
{
|
||||||
public string token { get; set; }
|
public required string currentPassword { get; set; }
|
||||||
public string currentPassword { get; set; }
|
|
||||||
|
|
||||||
public string? newPassword { get; set; }
|
public string? newPassword { get; set; }
|
||||||
public string? username { get; set; }
|
public string? username { get; set; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
|||||||
|
|
||||||
public class VerifyPayload
|
public class VerifyPayload
|
||||||
{
|
{
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string password { get; set; }
|
public required string password { get; set; }
|
||||||
public string code { get; set; }
|
public required string code { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class AdminUpdateBotResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class AdminUpdateUserResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class AdminUsersResponse
|
public class AdminUsersResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public int? totalPageCount { get; set; } = null;
|
public int? totalPageCount { get; set; } = null;
|
||||||
public AdminUser[]? users { get; set; }
|
public AdminUser[]? users { get; set; }
|
||||||
}
|
}
|
||||||
@@ -11,13 +9,13 @@ public class AdminUsersResponse
|
|||||||
public class AdminUser
|
public class AdminUser
|
||||||
{
|
{
|
||||||
public uint id { get; set; }
|
public uint id { get; set; }
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public string email { get; set; }
|
public required string email { get; set; }
|
||||||
public int rank { get; set; }
|
public required uint rank { get; set; }
|
||||||
public bool banned { get; set; }
|
public bool banned { get; set; }
|
||||||
public string banReason { get; set; }
|
public required string banReason { get; set; }
|
||||||
public string dateOfBirth { get; set; }
|
public required string dateOfBirth { get; set; }
|
||||||
public string dateJoined { get; set; }
|
public required string dateJoined { get; set; }
|
||||||
public string registrationIp { get; set; }
|
public required string registrationIp { get; set; }
|
||||||
public bool developer { get; set; }
|
public bool developer { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class IPBanResponse
|
public class ApiResponse {
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
public bool success { get; set; }
|
||||||
public string? error { get; set; }
|
public string? error { get; set; }
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|||||||
public class AuthServerInformation
|
public class AuthServerInformation
|
||||||
{
|
{
|
||||||
public bool registrationOpen { get; set; }
|
public bool registrationOpen { get; set; }
|
||||||
public AuthServerInformationCaptcha hcaptcha { get; set; }
|
public required AuthServerInformationCaptcha hcaptcha { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthServerInformationCaptcha
|
public class AuthServerInformationCaptcha
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class BanUserResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class CreateBotResponse
|
public class CreateBotResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public string? token { get; set; }
|
public string? token { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class JoinResponse
|
public class JoinResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public bool clientSuccess { get; set; } = false;
|
public bool clientSuccess { get; set; } = false;
|
||||||
public bool? banned { get; set; } = null;
|
public bool? banned { get; set; } = null;
|
||||||
public string? banReason { get; set; }
|
public string? banReason { get; set; }
|
||||||
public string? error { get; set; }
|
|
||||||
public string? username { get; set; }
|
public string? username { get; set; }
|
||||||
public Rank? rank { get; set; }
|
public uint? rank { get; set; }
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,8 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|||||||
public class ListBot
|
public class ListBot
|
||||||
{
|
{
|
||||||
public int id { get; set; }
|
public int id { get; set; }
|
||||||
public string username { get; set; }
|
public required string username { get; set; }
|
||||||
public int rank { get; set; }
|
public uint rank { get; set; }
|
||||||
public string owner { get; set; }
|
public required string owner { get; set; }
|
||||||
public string created { get; set; }
|
public required string created { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class ListBotsResponse
|
public class ListBotsResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public int? totalPageCount { get; set; } = null;
|
public int? totalPageCount { get; set; } = null;
|
||||||
public ListBot[]? bots { get; set; }
|
public ListBot[]? bots { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class LoginResponse
|
public class LoginResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? token { get; set; }
|
public string? token { get; set; }
|
||||||
public string? error { get; set; }
|
|
||||||
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; }
|
public uint rank { get; set; }
|
||||||
public bool? developer { get; set; } = null;
|
public bool? developer { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class LogoutResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class RegisterResponse
|
public class RegisterResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public bool? verificationRequired { get; set; } = null;
|
public bool? verificationRequired { get; set; } = null;
|
||||||
public string? username { get; set; }
|
public string? username { get; set; }
|
||||||
public string? email { get; set; }
|
public string? email { get; set; }
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class ResetPasswordResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
|
||||||
|
|
||||||
public class SendResetEmailResponse
|
|
||||||
{
|
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class SessionResponse
|
public class SessionResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
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; }
|
public uint rank { get; set; }
|
||||||
public bool? developer { get; set; } = null;
|
public bool? developer { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class UpdateResponse
|
public class UpdateResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public bool? verificationRequired { get; set; } = null;
|
public bool? verificationRequired { get; set; } = null;
|
||||||
public bool? sessionExpired { get; set; } = null;
|
public bool? sessionExpired { get; set; } = null;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||||
|
|
||||||
public class VerifyResponse
|
public class VerifyResponse : ApiResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
|
||||||
public string? error { get; set; }
|
|
||||||
public string? sessionToken { get; set; }
|
public string? sessionToken { get; set; }
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,30 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MySqlConnector;
|
||||||
|
using Tomlet;
|
||||||
|
using Tomlet.Attributes;
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
public class IConfig
|
public class IConfig
|
||||||
{
|
{
|
||||||
public RegistrationConfig Registration { get; set; }
|
public RegistrationConfig? Registration { get; set; }
|
||||||
public AccountConfig Accounts { get; set; }
|
public AccountConfig? Accounts { get; set; }
|
||||||
public CollabVMConfig CollabVM { get; set; }
|
public CollabVMConfig? CollabVM { get; set; }
|
||||||
public HTTPConfig HTTP { get; set; }
|
public HTTPConfig? HTTP { get; set; }
|
||||||
public MySQLConfig MySQL { get; set; }
|
public MySQLConfig? MySQL { get; set; }
|
||||||
public SMTPConfig SMTP { get; set; }
|
public SMTPConfig? SMTP { get; set; }
|
||||||
public hCaptchaConfig hCaptcha { get; set; }
|
public hCaptchaConfig? hCaptcha { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Load config instance from the specified toml file</summary>
|
||||||
|
public static IConfig Load(string configPath) {
|
||||||
|
// Load from disk
|
||||||
|
var configRaw = File.ReadAllText(configPath);
|
||||||
|
// Parse toml
|
||||||
|
var config = TomletMain.To<IConfig>(configRaw);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +32,7 @@ public class RegistrationConfig
|
|||||||
{
|
{
|
||||||
public bool EmailVerificationRequired { get; set; }
|
public bool EmailVerificationRequired { get; set; }
|
||||||
public bool EmailDomainWhitelist { get; set; }
|
public bool EmailDomainWhitelist { get; set; }
|
||||||
public string[] AllowedEmailDomains { get; set; }
|
public string[]? AllowedEmailDomains { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountConfig
|
public class AccountConfig
|
||||||
@@ -28,21 +44,38 @@ public class AccountConfig
|
|||||||
public class CollabVMConfig
|
public class CollabVMConfig
|
||||||
{
|
{
|
||||||
// We might want to move this to the database, but for now it's fine here.
|
// We might want to move this to the database, but for now it's fine here.
|
||||||
public string SecretKey { get; set; }
|
public string? SecretKey { get; set; }
|
||||||
}
|
}
|
||||||
public class HTTPConfig
|
public class HTTPConfig
|
||||||
{
|
{
|
||||||
public string Host { get; set; }
|
public string? Host { get; set; }
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public bool UseXForwardedFor { get; set; }
|
public bool UseXForwardedFor { get; set; }
|
||||||
public string[] TrustedProxies { get; set; }
|
public string[]? TrustedProxies { get; set; }
|
||||||
}
|
}
|
||||||
public class MySQLConfig
|
public class MySQLConfig
|
||||||
{
|
{
|
||||||
public string Host { get; set; }
|
[TomlNonSerialized]
|
||||||
public string Username { get; set; }
|
public string ConnectionString => new MySqlConnectionStringBuilder {
|
||||||
public string Password { get; set; }
|
Server = Host,
|
||||||
public string Database { get; set; }
|
UserID = Username,
|
||||||
|
Password = Password,
|
||||||
|
Database = Database
|
||||||
|
}.ConnectionString;
|
||||||
|
public string? Host { get; set; }
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
|
public string? Database { get; set; }
|
||||||
|
|
||||||
|
public DbContextOptionsBuilder<CollabVMAuthDbContext> Configure(DbContextOptionsBuilder<CollabVMAuthDbContext>? builder = null) {
|
||||||
|
return (builder ?? new DbContextOptionsBuilder<CollabVMAuthDbContext>())
|
||||||
|
.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbContextOptionsBuilder Configure(DbContextOptionsBuilder builder) {
|
||||||
|
return (builder ?? new DbContextOptionsBuilder<CollabVMAuthDbContext>())
|
||||||
|
.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SMTPConfig
|
public class SMTPConfig
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
|
||||||
|
|
||||||
public class IPBan
|
|
||||||
{
|
|
||||||
public IPAddress IP { get; set; }
|
|
||||||
public string Reason { get; set; }
|
|
||||||
public string? BannedBy { get; set; }
|
|
||||||
public DateTime BannedAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,15 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
public class Mailer
|
public class Mailer
|
||||||
{
|
{
|
||||||
private SMTPConfig Config;
|
private readonly SMTPConfig Config;
|
||||||
|
private readonly ILogger _logger;
|
||||||
public Mailer(SMTPConfig config)
|
public Mailer(SMTPConfig config)
|
||||||
{
|
{
|
||||||
if (config.Host == null || config.Port == null || config.Username == null || config.Password == null ||
|
if (config.Host == null || config.Port == null || config.Username == null || config.Password == null ||
|
||||||
@@ -16,10 +18,10 @@ public class Mailer
|
|||||||
config.VerificationCodeBody == null || config.ResetPasswordSubject == null ||
|
config.VerificationCodeBody == null || config.ResetPasswordSubject == null ||
|
||||||
config.ResetPasswordBody == null)
|
config.ResetPasswordBody == null)
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.FATAL,"SMTPConfig is missing required fields");
|
throw new InvalidOperationException("SMTPConfig is missing required fields");
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
}
|
||||||
Config = config;
|
Config = config;
|
||||||
|
_logger = LoggerFactory.Create(Utilities.ConfigureLogging).CreateLogger<Mailer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendVerificationCode(string username, string email, string code)
|
public async Task SendVerificationCode(string username, string email, string code)
|
||||||
@@ -27,23 +29,23 @@ public class Mailer
|
|||||||
var message = new MimeMessage();
|
var message = new MimeMessage();
|
||||||
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
||||||
message.To.Add(new MailboxAddress(username, email));
|
message.To.Add(new MailboxAddress(username, email));
|
||||||
message.Subject = Config.VerificationCodeSubject
|
message.Subject = Config.VerificationCodeSubject!
|
||||||
.Replace("$USERNAME", username)
|
.Replace("$USERNAME", username)
|
||||||
.Replace("$EMAIL", email)
|
.Replace("$EMAIL", email)
|
||||||
.Replace("$CODE", code);
|
.Replace("$CODE", code);
|
||||||
message.Body = new TextPart("plain")
|
message.Body = new TextPart("plain")
|
||||||
{
|
{
|
||||||
Text = Config.VerificationCodeBody
|
Text = Config.VerificationCodeBody!
|
||||||
.Replace("$USERNAME", username)
|
.Replace("$USERNAME", username)
|
||||||
.Replace("$EMAIL", email)
|
.Replace("$EMAIL", email)
|
||||||
.Replace("$CODE", code)
|
.Replace("$CODE", code)
|
||||||
};
|
};
|
||||||
using var client = new SmtpClient();
|
using var client = new SmtpClient();
|
||||||
await client.ConnectAsync(Config.Host, (int)Config.Port, SecureSocketOptions.StartTlsWhenAvailable);
|
await client.ConnectAsync(Config.Host, (int)Config.Port!, SecureSocketOptions.StartTlsWhenAvailable);
|
||||||
await client.AuthenticateAsync(Config.Username, Config.Password);
|
await client.AuthenticateAsync(Config.Username, Config.Password);
|
||||||
await client.SendAsync(message);
|
await client.SendAsync(message);
|
||||||
await client.DisconnectAsync(true);
|
await client.DisconnectAsync(true);
|
||||||
Utilities.Log(LogLevel.INFO, $"Sent e-mail verification code to {username} <{email}>");
|
_logger.LogInformation("Sent e-mail verification code to {username} <{email}>", username, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendPasswordResetEmail(string username, string email, string code)
|
public async Task SendPasswordResetEmail(string username, string email, string code)
|
||||||
@@ -51,23 +53,23 @@ public class Mailer
|
|||||||
var message = new MimeMessage();
|
var message = new MimeMessage();
|
||||||
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
||||||
message.To.Add(new MailboxAddress(username, email));
|
message.To.Add(new MailboxAddress(username, email));
|
||||||
message.Subject = Config.ResetPasswordSubject
|
message.Subject = Config.ResetPasswordSubject!
|
||||||
.Replace("$USERNAME", username)
|
.Replace("$USERNAME", username)
|
||||||
.Replace("$EMAIL", email)
|
.Replace("$EMAIL", email)
|
||||||
.Replace("$CODE", code);
|
.Replace("$CODE", code);
|
||||||
message.Body = new TextPart("plain")
|
message.Body = new TextPart("plain")
|
||||||
{
|
{
|
||||||
Text = Config.ResetPasswordBody
|
Text = Config.ResetPasswordBody!
|
||||||
.Replace("$USERNAME", username)
|
.Replace("$USERNAME", username)
|
||||||
.Replace("$EMAIL", email)
|
.Replace("$EMAIL", email)
|
||||||
.Replace("$CODE", code)
|
.Replace("$CODE", code)
|
||||||
};
|
};
|
||||||
using var client = new SmtpClient();
|
using var client = new SmtpClient();
|
||||||
await client.ConnectAsync(Config.Host, (int)Config.Port, SecureSocketOptions.StartTlsWhenAvailable);
|
await client.ConnectAsync(Config.Host, (int)Config.Port!, SecureSocketOptions.StartTlsWhenAvailable);
|
||||||
await client.AuthenticateAsync(Config.Username, Config.Password);
|
await client.AuthenticateAsync(Config.Username, Config.Password);
|
||||||
await client.SendAsync(message);
|
await client.SendAsync(message);
|
||||||
await client.DisconnectAsync(true);
|
await client.DisconnectAsync(true);
|
||||||
Utilities.Log(LogLevel.INFO, $"Sent password reset verification code to {username} <{email}>");
|
_logger.LogInformation("Sent password reset verification code to {username} <{email}>", username, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,110 +1,176 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.CommandLine;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Computernewb.CollabVMAuthServer.Database;
|
||||||
using Computernewb.CollabVMAuthServer.HTTP;
|
using Computernewb.CollabVMAuthServer.HTTP;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Tomlet;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
|
#pragma warning disable CS8618
|
||||||
public static IConfig Config { get; private set; }
|
public static IConfig Config { get; private set; }
|
||||||
public static Database Database { get; private set; }
|
|
||||||
public static hCaptchaClient? hCaptcha { get; private set; }
|
public static hCaptchaClient? hCaptcha { get; private set; }
|
||||||
public static Mailer? Mailer { get; private set; }
|
public static Mailer? Mailer { get; private set; }
|
||||||
public static string[] BannedPasswords { get; set; }
|
public static string[] BannedPasswords { get; set; }
|
||||||
|
#pragma warning restore CS8618
|
||||||
public static readonly Random Random = new Random();
|
public static readonly Random Random = new Random();
|
||||||
public static async Task Main(string[] args)
|
private static readonly ILogger _logger
|
||||||
|
= LoggerFactory.Create(Utilities.ConfigureLogging).CreateLogger<Program>();
|
||||||
|
|
||||||
|
public static async Task<int> Main(string[] args) {
|
||||||
|
if (EF.IsDesignTime) {
|
||||||
|
// We have a design time factory EF uses to handle this, just exit
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root command (run auth server)
|
||||||
|
var rootCommand = new RootCommand("CollabVM Authentication Server");
|
||||||
|
|
||||||
|
var configPathOption = new Option<string>(
|
||||||
|
name: "--config-path",
|
||||||
|
description: "Configuration file to use",
|
||||||
|
getDefaultValue: () => "./config.toml"
|
||||||
|
);
|
||||||
|
rootCommand.Add(configPathOption);
|
||||||
|
|
||||||
|
// Migrate DB
|
||||||
|
var migrateDbCommand = new Command("migrate-db", "Runs all pending database migrations");
|
||||||
|
rootCommand.Add(migrateDbCommand);
|
||||||
|
|
||||||
|
rootCommand.SetHandler(RunAuthServer, new AuthServerCliOptionsBinder(configPathOption));
|
||||||
|
migrateDbCommand.SetHandler(MigrateDatabase, new AuthServerCliOptionsBinder(configPathOption));
|
||||||
|
|
||||||
|
return await rootCommand.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> MigrateDatabase(AuthServerContext context) {
|
||||||
|
|
||||||
|
_logger.LogInformation("Running database migrations");
|
||||||
|
// Initialize database
|
||||||
|
var db = new CollabVMAuthDbContext(context.Config.MySQL!.Configure().Options);
|
||||||
|
// Detect and migrate legacy schema
|
||||||
|
await LegacyDbMigrator.CheckAndMigrate(db);
|
||||||
|
// Run migrations
|
||||||
|
_logger.LogInformation("Applying {cnt} migrations now...", (await db.Database.GetPendingMigrationsAsync()).Count());
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
_logger.LogInformation("Finished migrations.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> RunAuthServer(AuthServerContext context)
|
||||||
{
|
{
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
Utilities.Log(LogLevel.INFO, $"CollabVM Authentication Server v{ver.Major}.{ver.Minor}.{ver.Revision} starting up");
|
_logger.LogInformation("CollabVM Authentication Server v{major}.{minor}.{revision} starting up", ver!.Major, ver.Minor, ver.Revision);
|
||||||
// Read config.toml
|
// temp
|
||||||
string configraw;
|
Config = context.Config;
|
||||||
try
|
|
||||||
{
|
|
||||||
configraw = File.ReadAllText("config.toml");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Utilities.Log(LogLevel.FATAL, "Failed to read config.toml: " + ex.Message);
|
|
||||||
Environment.Exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Parse config.toml to IConfig
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Config = TomletMain.To<IConfig>(configraw);
|
|
||||||
} catch (Exception ex)
|
|
||||||
{
|
|
||||||
Utilities.Log(LogLevel.FATAL, "Failed to parse config.toml: " + ex.Message);
|
|
||||||
Environment.Exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
Database = new Database(Config.MySQL);
|
var db = new CollabVMAuthDbContext(context.Config.MySQL!.Configure().Options);
|
||||||
// Get version before initializing
|
// Make sure database schema is up-to-date, error if not
|
||||||
int dbversion = await Database.GetDatabaseVersion();
|
if ((await db.Database.GetPendingMigrationsAsync()).Any()) {
|
||||||
Utilities.Log(LogLevel.INFO, "Connected to database");
|
_logger.LogCritical("Database schema out of date. Please run migrations.");
|
||||||
Utilities.Log(LogLevel.INFO, dbversion == -1 ? "Initializing tables..." : $"Database version: {dbversion}");
|
return 1;
|
||||||
await Database.Init();
|
}
|
||||||
// If database was version 0, that should now be set, as versioning did not exist then
|
// Count users in database
|
||||||
if (dbversion == 0) await Database.SetDatabaseVersion(0);
|
var uc = await db.Users.CountAsync();
|
||||||
// If database was -1, that means it was just initialized and we should set it to the current version
|
_logger.LogInformation("{uc} users in database", uc);
|
||||||
if (dbversion == -1) await Database.SetDatabaseVersion(DatabaseUpdate.CurrentVersion);
|
if (uc == 0) _logger.LogWarning("No users in database, first user will be promoted to admin");
|
||||||
// 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");
|
|
||||||
// Init cron
|
// Init cron
|
||||||
await Cron.Start();
|
var cron = new Cron(context.Config.MySQL.Configure().Options);
|
||||||
|
await cron.Start();
|
||||||
// Create mailer
|
// Create mailer
|
||||||
if (!Config.SMTP.Enabled && Config.Registration.EmailVerificationRequired)
|
if (!Config.SMTP!.Enabled && Config.Registration!.EmailVerificationRequired)
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.FATAL, "Email verification is required but SMTP is disabled");
|
_logger.LogCritical("Email verification is required but SMTP is disabled");
|
||||||
Environment.Exit(1);
|
return 1;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
Mailer = Config.SMTP.Enabled ? new Mailer(Config.SMTP) : null;
|
Mailer = Config.SMTP.Enabled ? new Mailer(Config.SMTP) : null;
|
||||||
// Create hCaptcha client
|
// Create hCaptcha client
|
||||||
if (Config.hCaptcha.Enabled)
|
if (Config.hCaptcha!.Enabled)
|
||||||
{
|
{
|
||||||
hCaptcha = new hCaptchaClient(Config.hCaptcha.Secret!, Config.hCaptcha.SiteKey!);
|
hCaptcha = new hCaptchaClient(Config.hCaptcha.Secret!, Config.hCaptcha.SiteKey!);
|
||||||
Utilities.Log(LogLevel.INFO, "hCaptcha enabled");
|
_logger.LogInformation("hCaptcha enabled");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Utilities.Log(LogLevel.INFO, "hCaptcha disabled");
|
_logger.LogInformation("hCaptcha disabled");
|
||||||
}
|
}
|
||||||
// load password list
|
// load password list
|
||||||
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
||||||
// Configure web server
|
// Configure web server
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder();
|
||||||
#if DEBUG
|
Utilities.ConfigureLogging(builder.Logging);
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug);
|
|
||||||
#else
|
// Configure json serialization
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Warning);
|
builder.Services.AddControllers().AddJsonOptions((options) => {
|
||||||
#endif
|
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure database context
|
||||||
|
builder.Services.AddDbContext<CollabVMAuthDbContext>((builder) => context.Config.MySQL.Configure(builder));
|
||||||
|
|
||||||
|
// Configure forwarded headers
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>((options) => {
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
foreach (var proxy in context.Config.HTTP!.TrustedProxies!) {
|
||||||
|
options.KnownProxies.Add(IPAddress.Parse(proxy));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure authentication
|
||||||
|
builder.Services.AddAuthentication((options) => {
|
||||||
|
options.DefaultScheme = "CollabVM";
|
||||||
|
options.RequireAuthenticatedSignIn = false;
|
||||||
|
})
|
||||||
|
.AddScheme<CollabVMAuthenticationSchemeOptions, CollabVMAuthenticationHandler>("CollabVM", (options) => {
|
||||||
|
options.DbContextOptions = context.Config.MySQL.Configure().Options;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure authorization policies
|
||||||
|
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, CollabVMAuthorizationMiddlewareResultHandler>();
|
||||||
|
var authorization = builder.Services.AddAuthorizationBuilder();
|
||||||
|
authorization.AddPolicy("User", (policy) => {
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.RequireClaim("type", "user");
|
||||||
|
});
|
||||||
|
authorization.AddPolicy("Staff", (policy) => {
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.RequireClaim("rank", "2", "3");
|
||||||
|
});
|
||||||
|
authorization.AddPolicy("Developer", (policy) => {
|
||||||
|
policy.RequireAuthenticatedUser();
|
||||||
|
policy.RequireClaim("developer", "1");
|
||||||
|
});
|
||||||
|
|
||||||
builder.WebHost.UseKestrel(k =>
|
builder.WebHost.UseKestrel(k =>
|
||||||
{
|
{
|
||||||
k.Listen(IPAddress.Parse(Config.HTTP.Host), Config.HTTP.Port);
|
k.Listen(IPAddress.Parse(Config.HTTP!.Host!), Config.HTTP.Port);
|
||||||
});
|
});
|
||||||
builder.Services.AddCors();
|
builder.Services.AddCors();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
if (context.Config.HTTP!.UseXForwardedFor) {
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
}
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
// TODO: Make this more strict
|
// TODO: Make this more strict
|
||||||
app.UseCors(cors => cors.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
|
app.UseCors(cors => cors.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
|
||||||
app.Lifetime.ApplicationStarted.Register(() => Utilities.Log(LogLevel.INFO, $"Webserver listening on {Config.HTTP.Host}:{Config.HTTP.Port}"));
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
// Register routes
|
// Register routes
|
||||||
Routes.RegisterRoutes(app);
|
app.MapControllers();
|
||||||
AdminRoutes.RegisterRoutes(app);
|
await app.RunAsync();
|
||||||
DeveloperRoutes.RegisterRoutes(app);
|
return 0;
|
||||||
app.Run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
|
||||||
|
|
||||||
public class Session
|
|
||||||
{
|
|
||||||
public string Token { get; set; }
|
|
||||||
public string Username { get; set; }
|
|
||||||
public DateTime Created { get; set; }
|
|
||||||
public DateTime LastUsed { get; set; }
|
|
||||||
public IPAddress LastIP { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Computernewb.CollabVMAuthServer;
|
|
||||||
|
|
||||||
public class StaffMember
|
|
||||||
{
|
|
||||||
public string Username { get; set; }
|
|
||||||
public Rank Rank { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
|
||||||
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
public uint Id { get; set; }
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public DateOnly DateOfBirth { get; set; }
|
|
||||||
public bool EmailVerified { get; set; }
|
|
||||||
public string? EmailVerificationCode { get; set; }
|
|
||||||
public string? PasswordResetCode { get; set; }
|
|
||||||
public Rank Rank { get; set; }
|
|
||||||
public bool Banned { get; set; }
|
|
||||||
public string? BanReason { get; set; }
|
|
||||||
public IPAddress RegistrationIP { get; set; }
|
|
||||||
public DateTime Joined { get; set; }
|
|
||||||
public bool Developer { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Rank : uint
|
|
||||||
{
|
|
||||||
Registered = 1,
|
|
||||||
Admin = 2,
|
|
||||||
Moderator = 3,
|
|
||||||
}
|
|
||||||
@@ -1,75 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Computernewb.CollabVMAuthServer;
|
namespace Computernewb.CollabVMAuthServer;
|
||||||
|
|
||||||
public enum LogLevel
|
|
||||||
{
|
|
||||||
DEBUG,
|
|
||||||
INFO,
|
|
||||||
WARN,
|
|
||||||
ERROR,
|
|
||||||
FATAL
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class Utilities
|
public static class Utilities
|
||||||
{
|
{
|
||||||
public static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions
|
public static void ConfigureLogging(ILoggingBuilder builder) {
|
||||||
{
|
builder.ClearProviders();
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
builder.AddConsole();
|
||||||
};
|
#if DEBUG
|
||||||
public static void Log(LogLevel level, string msg)
|
builder.SetMinimumLevel(LogLevel.Debug);
|
||||||
{
|
#else
|
||||||
#if !DEBUG
|
builder.SetMinimumLevel(LogLevel.Warning);
|
||||||
if (level == LogLevel.DEBUG)
|
#endif
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
StringBuilder logstr = new StringBuilder();
|
|
||||||
logstr.Append("[");
|
|
||||||
logstr.Append(DateTime.Now.ToString("G"));
|
|
||||||
logstr.Append("] [");
|
|
||||||
switch (level)
|
|
||||||
{
|
|
||||||
case LogLevel.DEBUG:
|
|
||||||
logstr.Append("DEBUG");
|
|
||||||
break;
|
|
||||||
case LogLevel.INFO:
|
|
||||||
logstr.Append("INFO");
|
|
||||||
break;
|
|
||||||
case LogLevel.WARN:
|
|
||||||
logstr.Append("WARN");
|
|
||||||
break;
|
|
||||||
case LogLevel.ERROR:
|
|
||||||
logstr.Append("ERROR");
|
|
||||||
break;
|
|
||||||
case LogLevel.FATAL:
|
|
||||||
logstr.Append("FATAL");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException("Invalid log level");
|
|
||||||
}
|
|
||||||
logstr.Append("] ");
|
|
||||||
logstr.Append(msg);
|
|
||||||
switch (level)
|
|
||||||
{
|
|
||||||
case LogLevel.DEBUG:
|
|
||||||
case LogLevel.INFO:
|
|
||||||
Console.WriteLine(logstr.ToString());
|
|
||||||
break;
|
|
||||||
case LogLevel.WARN:
|
|
||||||
case LogLevel.ERROR:
|
|
||||||
case LogLevel.FATAL:
|
|
||||||
Console.Error.WriteLine(logstr.ToString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ValidateUsername(string username)
|
public static bool ValidateUsername(string username)
|
||||||
@@ -101,64 +46,4 @@ public static class Utilities
|
|||||||
}
|
}
|
||||||
return str.ToString();
|
return str.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IPAddress? GetIP(HttpContext ctx)
|
|
||||||
{
|
|
||||||
if (Program.Config.HTTP.UseXForwardedFor)
|
|
||||||
{
|
|
||||||
if (!Program.Config.HTTP.TrustedProxies.Contains(ctx.Connection.RemoteIpAddress.ToString()))
|
|
||||||
{
|
|
||||||
Utilities.Log(LogLevel.WARN,
|
|
||||||
$"An IP address not allowed to proxy connections ({ctx.Connection.RemoteIpAddress.ToString()}) attempted to connect. This means your server port is exposed to the internet.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.Request.Headers["X-Forwarded-For"].Count == 0)
|
|
||||||
{
|
|
||||||
Utilities.Log(LogLevel.WARN, $"Missing X-Forwarded-For header in request from {ctx.Connection.RemoteIpAddress.ToString()}. This is probably a misconfiguration of your reverse proxy.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IPAddress.TryParse(ctx.Request.Headers["X-Forwarded-For"][0], out var ip)) return null;
|
|
||||||
return ip;
|
|
||||||
}
|
|
||||||
else return ctx.Connection.RemoteIpAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsSessionExpired(Session session)
|
|
||||||
{
|
|
||||||
return DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<StaffMember?> GetStaffByToken(string token)
|
|
||||||
{
|
|
||||||
if (token.Length == 32)
|
|
||||||
{
|
|
||||||
// User
|
|
||||||
var session = await Program.Database.GetSession(token);
|
|
||||||
if (session == null || Utilities.IsSessionExpired(session)) return null;
|
|
||||||
var user = await Program.Database.GetUser(session.Username);
|
|
||||||
if (user == null) return null;
|
|
||||||
return new StaffMember
|
|
||||||
{
|
|
||||||
Username = user.Username,
|
|
||||||
Rank = user.Rank
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (token.Length == 64)
|
|
||||||
{
|
|
||||||
// Bot
|
|
||||||
var bot = await Program.Database.GetBot(token: token);
|
|
||||||
if (bot == null) return null;
|
|
||||||
return new StaffMember
|
|
||||||
{
|
|
||||||
Username = bot.Username,
|
|
||||||
Rank = bot.Rank
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -36,8 +36,8 @@ public class hCaptchaClient
|
|||||||
public class hCaptchaResponse
|
public class hCaptchaResponse
|
||||||
{
|
{
|
||||||
public bool success { get; set; }
|
public bool success { get; set; }
|
||||||
public string challenge_ts { get; set; }
|
public string? challenge_ts { get; set; }
|
||||||
public string hostname { get; set; }
|
public string? hostname { get; set; }
|
||||||
public bool? credit { get; set; }
|
public bool? credit { get; set; }
|
||||||
[JsonPropertyName("error-codes")]
|
[JsonPropertyName("error-codes")]
|
||||||
public string[]? error_codes { get; set; }
|
public string[]? error_codes { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user