Compare commits
10 Commits
130baa8863
...
a7df221777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7df221777 | ||
|
|
ce847c2207 | ||
|
|
290a9a5777 | ||
|
|
4b43dd833b | ||
|
|
2d687c1440 | ||
|
|
1a3224ab0b | ||
|
|
e5c136a227 | ||
|
|
4f17c1e58f | ||
|
|
1ab7dd0626 | ||
|
|
c7f3cb3441 |
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/CollabVMAuthServer/bin/Debug/net8.0/CollabVMAuthServer.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/CollabVMAuthServer/CollabVMAuthServer.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/CollabVMAuthServer/CollabVMAuthServer.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/CollabVMAuthServer/CollabVMAuthServer.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
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,11 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<PublishAot>false</PublishAot>
|
||||
<RootNamespace>Computernewb.CollabVMAuthServer</RootNamespace>
|
||||
<Company>Computernewb Development Team</Company>
|
||||
<AssemblyVersion>1.1</AssemblyVersion>
|
||||
<AssemblyVersion>2.0</AssemblyVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.4.0" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.3.6" />
|
||||
<PackageReference Include="Samboy063.Tomlet" Version="5.3.1" />
|
||||
<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="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||
<PackageReference Include="Samboy063.Tomlet" Version="6.0.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
68
CollabVMAuthServer/Cron.cs
Normal file
68
CollabVMAuthServer/Cron.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Computernewb.CollabVMAuthServer.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class Cron
|
||||
{
|
||||
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>();
|
||||
}
|
||||
|
||||
private Timer timer = new Timer();
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
#if DEBUG
|
||||
timer.Interval = 1000 * 60; // 60 seconds
|
||||
#else
|
||||
timer.Interval = 1000 * 60 * 10; // 10 minutes
|
||||
#endif
|
||||
timer.Elapsed += async (sender, e) => await RunAll();
|
||||
await RunAll();
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
timer.Stop();
|
||||
timer.Interval = 1000 * 60 * 10;
|
||||
}
|
||||
|
||||
public async Task RunAll()
|
||||
{
|
||||
_logger.LogDebug("Running all cron jobs");
|
||||
var t = new List<Task>();
|
||||
t.Add(PurgeOldSessions());
|
||||
if (Program.Config.Registration!.EmailVerificationRequired) t.Add(ExpireAccounts());
|
||||
await Task.WhenAll(t);
|
||||
_logger.LogDebug("Finished running all cron jobs");
|
||||
}
|
||||
// Expire unverified accounts after 2 days. Don't purge if the code is null
|
||||
public async Task ExpireAccounts()
|
||||
{
|
||||
_logger.LogDebug("Purging unverified accounts");
|
||||
using var dbContext = new CollabVMAuthDbContext(_dbContextOptions);
|
||||
dbContext.Users.RemoveRange(dbContext.Users.Where(u => (!u.EmailVerified) && u.EmailVerificationCode != null && (u.Created < DateTime.UtcNow.AddDays(-2))));
|
||||
var a = await dbContext.SaveChangesAsync();
|
||||
_logger.LogInformation("Purged {a} unverified accounts", a);
|
||||
}
|
||||
|
||||
public async Task PurgeOldSessions()
|
||||
{
|
||||
_logger.LogDebug("Purging old sessions");
|
||||
using var dbContext = new CollabVMAuthDbContext(_dbContextOptions);
|
||||
dbContext.Sessions.RemoveRange(dbContext.Sessions.Where(s => s.LastUsed < DateTime.UtcNow - TimeSpan.FromDays(Program.Config.Accounts!.SessionExpiryDays)));
|
||||
var a = await dbContext.SaveChangesAsync();
|
||||
_logger.LogInformation("Purged {a} old sessions", a);
|
||||
}
|
||||
}
|
||||
@@ -1,550 +0,0 @@
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
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, string orderBy = "id", bool descending = false)
|
||||
{
|
||||
await using var db = new MySqlConnection(connectionString);
|
||||
await db.OpenAsync();
|
||||
await using var cmd = db.CreateCommand();
|
||||
var where = new List<string>();
|
||||
if (filterUsername != null)
|
||||
{
|
||||
where.Add("username LIKE @filterUsername");
|
||||
cmd.Parameters.AddWithValue("@filterUsername", filterUsername);
|
||||
}
|
||||
cmd.CommandText = $"SELECT * FROM users {(where.Count > 0 ? "WHERE" : "")} {string.Join(" AND ", where)} ORDER BY {orderBy} {(descending ? "DESC" : "ASC")}";
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
var users = new List<User>();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
users.Add(new User
|
||||
{
|
||||
Id = reader.GetUInt32("id"),
|
||||
Username = reader.GetString("username"),
|
||||
Password = reader.GetString("password"),
|
||||
Email = reader.GetString("email"),
|
||||
DateOfBirth = reader.GetDateOnly("date_of_birth"),
|
||||
EmailVerified = reader.GetBoolean("email_verified"),
|
||||
EmailVerificationCode = reader.IsDBNull("email_verification_code") ? null : reader.GetString("email_verification_code"),
|
||||
PasswordResetCode = reader.IsDBNull("password_reset_code") ? null : reader.GetString("password_reset_code"),
|
||||
Rank = (Rank)reader.GetUInt32("cvm_rank"),
|
||||
Banned = reader.GetBoolean("banned"),
|
||||
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 ExecuteNonQuery(string query)
|
||||
{
|
||||
await using var db = new MySqlConnection(connectionString);
|
||||
await db.OpenAsync();
|
||||
await using var cmd = db.CreateCommand();
|
||||
cmd.CommandText = query;
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public async Task SetBanned(string username, bool banned, string? reason)
|
||||
{
|
||||
await using var db = new MySqlConnection(connectionString);
|
||||
await db.OpenAsync();
|
||||
await using var cmd = db.CreateCommand();
|
||||
cmd.CommandText = "UPDATE users SET banned = @banned, ban_reason = @reason WHERE username = @username";
|
||||
cmd.Parameters.AddWithValue("@banned", banned);
|
||||
cmd.Parameters.AddWithValue("@reason", reason);
|
||||
cmd.Parameters.AddWithValue("@username", username);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public async Task<long> CountUsers()
|
||||
{
|
||||
await using var db = new MySqlConnection(connectionString);
|
||||
await db.OpenAsync();
|
||||
await using var cmd = db.CreateCommand();
|
||||
cmd.CommandText = "SELECT COUNT(*) FROM users";
|
||||
return (long)await cmd.ExecuteScalarAsync();
|
||||
}
|
||||
}
|
||||
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,33 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public static class DatabaseUpdate
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
|
||||
private static ReadOnlyDictionary<int, Func<Database, Task>> Updates = new Dictionary<int, Func<Database, Task>>()
|
||||
{
|
||||
{ 1, async db =>
|
||||
{
|
||||
// Update to version 1
|
||||
// Add ban_reason column to users table
|
||||
await db.ExecuteNonQuery("ALTER TABLE users ADD COLUMN ban_reason TEXT DEFAULT NULL");
|
||||
}},
|
||||
}.AsReadOnly();
|
||||
|
||||
public async static Task Update(Database db)
|
||||
{
|
||||
var version = await db.GetDatabaseVersion();
|
||||
if (version == -1) throw new InvalidOperationException("Uninitialized database cannot be updated");
|
||||
if (version == CurrentVersion) return;
|
||||
if (version > CurrentVersion) throw new InvalidOperationException("Database version is newer than the server supports");
|
||||
Utilities.Log(LogLevel.INFO, $"Updating database from version {version} to {CurrentVersion}");
|
||||
for (int i = version + 1; i <= CurrentVersion; i++)
|
||||
{
|
||||
if (!Updates.TryGetValue(i, out var update)) throw new InvalidOperationException($"No update available for version {i}");
|
||||
await update(db);
|
||||
}
|
||||
await db.SetDatabaseVersion(CurrentVersion);
|
||||
}
|
||||
}
|
||||
@@ -1,494 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer.HTTP;
|
||||
|
||||
public static class AdminRoutes
|
||||
{
|
||||
public static void RegisterRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("/api/v1/admin/users", (Delegate)HandleAdminUsers);
|
||||
app.MapPost("/api/v1/admin/updateuser", (Delegate)HandleAdminUpdateUser);
|
||||
app.MapPost("/api/v1/admin/updatebot", (Delegate)HandleAdminUpdateBot);
|
||||
app.MapPost("/api/v1/admin/ban", (Delegate)HandleBanUser);
|
||||
app.MapPost("/api/v1/admin/ipban", (Delegate)HandleIPBan);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleIPBan(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
IPBanPayload? payload;
|
||||
try
|
||||
{
|
||||
payload = await context.Request.ReadFromJsonAsync<IPBanPayload>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.session) || string.IsNullOrWhiteSpace(payload.ip) || (payload.banned && string.IsNullOrWhiteSpace(payload.reason)) || payload.banned == null || !IPAddress.TryParse(payload.ip, out var ip))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check token
|
||||
var session = await Program.Database.GetSession(payload.session);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("Could not lookup user from session");
|
||||
if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Insufficient permissions"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Set ban
|
||||
if (payload.banned)
|
||||
{
|
||||
await Program.Database.BanIP(ip, payload.reason, user.Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Program.Database.UnbanIP(ip);
|
||||
}
|
||||
return Results.Json(new IPBanResponse
|
||||
{
|
||||
success = true
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleBanUser(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
BanUserPayload? payload;
|
||||
try
|
||||
{
|
||||
payload = await context.Request.ReadFromJsonAsync<BanUserPayload>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username) || (payload.banned && string.IsNullOrWhiteSpace(payload.reason)) || payload.banned == null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check token
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("Could not lookup user from session");
|
||||
if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Insufficient permissions"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check target user
|
||||
var targetUser = await Program.Database.GetUser(payload.username);
|
||||
if (targetUser == null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "User not found"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Set ban
|
||||
await Program.Database.SetBanned(targetUser.Username, payload.banned, payload.banned ? payload.reason : null);
|
||||
return Results.Json(new BanUserResponse
|
||||
{
|
||||
success = true
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAdminUpdateBot(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
AdminUpdateBotPayload? payload;
|
||||
try
|
||||
{
|
||||
payload = await context.Request.ReadFromJsonAsync<AdminUpdateBotPayload>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token) || string.IsNullOrWhiteSpace(payload.username))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check token
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("Could not lookup user from session");
|
||||
if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new AdminUsersResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Insufficient permissions"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check target bot
|
||||
var targetBot = await Program.Database.GetBot(payload.username);
|
||||
if (targetBot == null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Bot not found"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Make sure at least one field is being updated
|
||||
if (payload.rank == null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateBotResponse
|
||||
{
|
||||
success = false,
|
||||
error = "No fields to update"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// 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 session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("Could not lookup user from session");
|
||||
if (user.Rank != Rank.Admin)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new AdminUsersResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Insufficient permissions"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check target user
|
||||
var targetUser = await Program.Database.GetUser(payload.username);
|
||||
if (targetUser == null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "User not found"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
int? rank = payload.rank;
|
||||
if (rank != null && rank < 1 || rank > 3)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUpdateUserResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid rank"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// 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 session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUsersResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check rank
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("Could not lookup user from session");
|
||||
if (user.Rank != Rank.Admin && user.Rank != Rank.Moderator)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new AdminUsersResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Insufficient permissions"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Validate orderBy
|
||||
if (payload.orderBy != null && !new string[] { "id", "username", "email", "date_of_birth", "cvm_rank", "banned", "created" }.Contains(payload.orderBy))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new AdminUsersResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid orderBy"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Get users
|
||||
string? filterUsername = null;
|
||||
if (payload.filterUsername != null)
|
||||
{
|
||||
filterUsername = "%" + payload.filterUsername
|
||||
.Replace("%", "!%")
|
||||
.Replace("!", "!!")
|
||||
.Replace("_", "!_")
|
||||
.Replace("[", "![") + "%";
|
||||
}
|
||||
var users = (await Program.Database.ListUsers(filterUsername, payload.orderBy ?? "id", payload.orderByDescending)).Select(user => new AdminUser
|
||||
{
|
||||
id = user.Id,
|
||||
username = user.Username,
|
||||
email = user.Email,
|
||||
rank = (int)user.Rank,
|
||||
banned = user.Banned,
|
||||
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,196 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
using Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer.HTTP;
|
||||
|
||||
public static class DeveloperRoutes
|
||||
{
|
||||
public static void RegisterRoutes(IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapPost("/api/v1/bots/create", (Delegate)HandleCreateBot);
|
||||
app.MapPost("/api/v1/bots/list", (Delegate)HandleListBots);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleListBots(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new ListBotsResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
ListBotsPayload? payload;
|
||||
try
|
||||
{
|
||||
payload = await context.Request.ReadFromJsonAsync<ListBotsPayload>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Utilities.Log(LogLevel.DEBUG, $"Failed to parse JSON: {ex.Message}");
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new ListBotsResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token) || payload.resultsPerPage <= 0 ||
|
||||
payload.page <= 0)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new ListBotsResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check token
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || Utilities.IsSessionExpired(session))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new ListBotsResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check developer status
|
||||
var user = await Program.Database.GetUser(session.Username) ??
|
||||
throw new Exception("Unable to get user from session");
|
||||
if (!user.Developer && user.Rank != Rank.Admin && 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 string token { get; set; }
|
||||
public string username { get; set; }
|
||||
public int? rank { get; set; }
|
||||
public required string username { get; set; }
|
||||
public uint? rank { get; set; }
|
||||
}
|
||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class AdminUpdateUserPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
public string username { get; set; }
|
||||
public int? rank { get; set; }
|
||||
public required string username { get; set; }
|
||||
public uint? rank { get; set; }
|
||||
public bool? developer { get; set; } = null;
|
||||
}
|
||||
@@ -2,10 +2,10 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class AdminUsersPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
public int resultsPerPage { get; set; }
|
||||
public int page { get; set; }
|
||||
public string? filterUsername { get; set; }
|
||||
public string? filterIp { get; set; }
|
||||
public string? orderBy { get; set; }
|
||||
public bool orderByDescending { get; set; } = false;
|
||||
}
|
||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class BanUserPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
public string username { get; set; }
|
||||
public required string username { 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 string token { get; set; }
|
||||
public string username { get; set; }
|
||||
public required string username { get; set; }
|
||||
}
|
||||
@@ -2,8 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class IPBanPayload
|
||||
{
|
||||
public string session { get; set; }
|
||||
public string ip { get; set; }
|
||||
public required string ip { 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 string secretKey { get; set; }
|
||||
public string sessionToken { get; set; }
|
||||
public string ip { get; set; }
|
||||
public required string secretKey { get; set; }
|
||||
public required string sessionToken { get; set; }
|
||||
public required string ip { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,6 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class ListBotsPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
public int resultsPerPage { get; set; }
|
||||
public int page { get; set; }
|
||||
public string? owner { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class LoginPayload
|
||||
{
|
||||
public string username { get; set; }
|
||||
public string password { get; set; }
|
||||
public required string username { get; set; }
|
||||
public required string password { 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 string username { get; set; }
|
||||
public string password { get; set; }
|
||||
public string email { get; set; }
|
||||
public required string username { get; set; }
|
||||
public required string password { get; set; }
|
||||
public required string email { 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 string username { get; set; }
|
||||
public string email { get; set; }
|
||||
public string code { get; set; }
|
||||
public string newPassword { get; set; }
|
||||
public required string username { get; set; }
|
||||
public required string email { get; set; }
|
||||
public required string code { get; set; }
|
||||
public required string newPassword { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class SendResetEmailPayload
|
||||
{
|
||||
public string email { get; set; }
|
||||
public string username { get; set; }
|
||||
public required string email { get; set; }
|
||||
public required string username { 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 string token { get; set; }
|
||||
public string currentPassword { get; set; }
|
||||
public required string currentPassword { get; set; }
|
||||
|
||||
public string? newPassword { get; set; }
|
||||
public string? username { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Payloads;
|
||||
|
||||
public class VerifyPayload
|
||||
{
|
||||
public string username { get; set; }
|
||||
public string password { get; set; }
|
||||
public string code { get; set; }
|
||||
public required string username { get; set; }
|
||||
public required string password { 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;
|
||||
|
||||
public class AdminUsersResponse
|
||||
public class AdminUsersResponse : ApiResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public int? totalPageCount { get; set; } = null;
|
||||
public AdminUser[]? users { get; set; }
|
||||
}
|
||||
@@ -11,13 +9,13 @@ public class AdminUsersResponse
|
||||
public class AdminUser
|
||||
{
|
||||
public uint id { get; set; }
|
||||
public string username { get; set; }
|
||||
public string email { get; set; }
|
||||
public int rank { get; set; }
|
||||
public required string username { get; set; }
|
||||
public required string email { get; set; }
|
||||
public required uint rank { get; set; }
|
||||
public bool banned { get; set; }
|
||||
public string banReason { get; set; }
|
||||
public string dateOfBirth { get; set; }
|
||||
public string dateJoined { get; set; }
|
||||
public string registrationIp { get; set; }
|
||||
public required string banReason { get; set; }
|
||||
public required string dateOfBirth { get; set; }
|
||||
public required string dateJoined { get; set; }
|
||||
public required string registrationIp { get; set; }
|
||||
public bool developer { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||
|
||||
public class IPBanResponse
|
||||
{
|
||||
public class ApiResponse {
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
}
|
||||
@@ -3,7 +3,7 @@ namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||
public class AuthServerInformation
|
||||
{
|
||||
public bool registrationOpen { get; set; }
|
||||
public AuthServerInformationCaptcha hcaptcha { get; set; }
|
||||
public required AuthServerInformationCaptcha hcaptcha { get; set; }
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
public class CreateBotResponse
|
||||
public class CreateBotResponse : ApiResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public string? token { get; set; }
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
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? banned { get; set; } = null;
|
||||
public string? banReason { get; set; }
|
||||
public string? error { 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 int id { get; set; }
|
||||
public string username { get; set; }
|
||||
public int rank { get; set; }
|
||||
public string owner { get; set; }
|
||||
public string created { get; set; }
|
||||
public required string username { get; set; }
|
||||
public uint rank { get; set; }
|
||||
public required string owner { get; set; }
|
||||
public required string created { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
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 ListBot[]? bots { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
namespace Computernewb.CollabVMAuthServer.HTTP.Responses;
|
||||
|
||||
public class LoginResponse
|
||||
public class LoginResponse : ApiResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? token { get; set; }
|
||||
public string? error { get; set; }
|
||||
public bool? verificationRequired { get; set; }
|
||||
public string? email { get; set; }
|
||||
public string? username { get; set; }
|
||||
public int rank { get; set; }
|
||||
public uint rank { get; set; }
|
||||
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;
|
||||
|
||||
public class RegisterResponse
|
||||
public class RegisterResponse : ApiResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public bool? verificationRequired { get; set; } = null;
|
||||
public string? username { 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;
|
||||
|
||||
public class SessionResponse
|
||||
public class SessionResponse : ApiResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public bool banned { get; set; } = false;
|
||||
public string? username { get; set; }
|
||||
public string? email { get; set; }
|
||||
public int rank { get; set; }
|
||||
public uint rank { get; set; }
|
||||
public bool? developer { get; set; } = null;
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
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? sessionExpired { get; set; } = null;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
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; }
|
||||
}
|
||||
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;
|
||||
|
||||
public class IConfig
|
||||
{
|
||||
public RegistrationConfig Registration { get; set; }
|
||||
public AccountConfig Accounts { get; set; }
|
||||
public CollabVMConfig CollabVM { get; set; }
|
||||
public HTTPConfig HTTP { get; set; }
|
||||
public MySQLConfig MySQL { get; set; }
|
||||
public SMTPConfig SMTP { get; set; }
|
||||
public hCaptchaConfig hCaptcha { get; set; }
|
||||
public RegistrationConfig? Registration { get; set; }
|
||||
public AccountConfig? Accounts { get; set; }
|
||||
public CollabVMConfig? CollabVM { get; set; }
|
||||
public HTTPConfig? HTTP { get; set; }
|
||||
public MySQLConfig? MySQL { get; set; }
|
||||
public SMTPConfig? SMTP { 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 EmailDomainWhitelist { get; set; }
|
||||
public string[] AllowedEmailDomains { get; set; }
|
||||
public string[]? AllowedEmailDomains { get; set; }
|
||||
}
|
||||
|
||||
public class AccountConfig
|
||||
@@ -28,21 +44,38 @@ public class AccountConfig
|
||||
public class CollabVMConfig
|
||||
{
|
||||
// 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 string Host { get; set; }
|
||||
public string? Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool UseXForwardedFor { get; set; }
|
||||
public string[] TrustedProxies { get; set; }
|
||||
public string[]? TrustedProxies { get; set; }
|
||||
}
|
||||
public class MySQLConfig
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Database { get; set; }
|
||||
[TomlNonSerialized]
|
||||
public string ConnectionString => new MySqlConnectionStringBuilder {
|
||||
Server = Host,
|
||||
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
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class Mailer
|
||||
{
|
||||
private SMTPConfig Config;
|
||||
private readonly SMTPConfig Config;
|
||||
private readonly ILogger _logger;
|
||||
public Mailer(SMTPConfig config)
|
||||
{
|
||||
if (config.Host == null || config.Port == null || config.Username == null || config.Password == null ||
|
||||
@@ -14,10 +18,10 @@ public class Mailer
|
||||
config.VerificationCodeBody == null || config.ResetPasswordSubject == null ||
|
||||
config.ResetPasswordBody == null)
|
||||
{
|
||||
Utilities.Log(LogLevel.FATAL,"SMTPConfig is missing required fields");
|
||||
Environment.Exit(1);
|
||||
throw new InvalidOperationException("SMTPConfig is missing required fields");
|
||||
}
|
||||
Config = config;
|
||||
_logger = LoggerFactory.Create(Utilities.ConfigureLogging).CreateLogger<Mailer>();
|
||||
}
|
||||
|
||||
public async Task SendVerificationCode(string username, string email, string code)
|
||||
@@ -25,23 +29,23 @@ public class Mailer
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
||||
message.To.Add(new MailboxAddress(username, email));
|
||||
message.Subject = Config.VerificationCodeSubject
|
||||
message.Subject = Config.VerificationCodeSubject!
|
||||
.Replace("$USERNAME", username)
|
||||
.Replace("$EMAIL", email)
|
||||
.Replace("$CODE", code);
|
||||
message.Body = new TextPart("plain")
|
||||
{
|
||||
Text = Config.VerificationCodeBody
|
||||
Text = Config.VerificationCodeBody!
|
||||
.Replace("$USERNAME", username)
|
||||
.Replace("$EMAIL", email)
|
||||
.Replace("$CODE", code)
|
||||
};
|
||||
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.SendAsync(message);
|
||||
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)
|
||||
@@ -49,23 +53,23 @@ public class Mailer
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(Config.FromName, Config.FromEmail));
|
||||
message.To.Add(new MailboxAddress(username, email));
|
||||
message.Subject = Config.ResetPasswordSubject
|
||||
message.Subject = Config.ResetPasswordSubject!
|
||||
.Replace("$USERNAME", username)
|
||||
.Replace("$EMAIL", email)
|
||||
.Replace("$CODE", code);
|
||||
message.Body = new TextPart("plain")
|
||||
{
|
||||
Text = Config.ResetPasswordBody
|
||||
Text = Config.ResetPasswordBody!
|
||||
.Replace("$USERNAME", username)
|
||||
.Replace("$EMAIL", email)
|
||||
.Replace("$CODE", code)
|
||||
};
|
||||
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.SendAsync(message);
|
||||
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,101 +1,176 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Computernewb.CollabVMAuthServer.Database;
|
||||
using Computernewb.CollabVMAuthServer.HTTP;
|
||||
using Tomlet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class Program
|
||||
{
|
||||
#pragma warning disable CS8618
|
||||
public static IConfig Config { get; private set; }
|
||||
public static Database Database { get; private set; }
|
||||
public static hCaptchaClient? hCaptcha { get; private set; }
|
||||
public static Mailer? Mailer { get; private set; }
|
||||
public static string[] BannedPasswords { get; set; }
|
||||
#pragma warning restore CS8618
|
||||
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;
|
||||
Utilities.Log(LogLevel.INFO, $"CollabVM Authentication Server v{ver.Major}.{ver.Minor}.{ver.Revision} starting up");
|
||||
// Read config.toml
|
||||
string configraw;
|
||||
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;
|
||||
}
|
||||
_logger.LogInformation("CollabVM Authentication Server v{major}.{minor}.{revision} starting up", ver!.Major, ver.Minor, ver.Revision);
|
||||
// temp
|
||||
Config = context.Config;
|
||||
// Initialize database
|
||||
Database = new Database(Config.MySQL);
|
||||
// Get version before initializing
|
||||
int dbversion = await Database.GetDatabaseVersion();
|
||||
Utilities.Log(LogLevel.INFO, "Connected to database");
|
||||
Utilities.Log(LogLevel.INFO, dbversion == -1 ? "Initializing tables..." : $"Database version: {dbversion}");
|
||||
await Database.Init();
|
||||
// If database was version 0, that should now be set, as versioning did not exist then
|
||||
if (dbversion == 0) await Database.SetDatabaseVersion(0);
|
||||
// If database was -1, that means it was just initialized and we should set it to the current version
|
||||
if (dbversion == -1) await Database.SetDatabaseVersion(DatabaseUpdate.CurrentVersion);
|
||||
// Perform any necessary database updates
|
||||
await DatabaseUpdate.Update(Database);
|
||||
var uc = await Database.CountUsers();
|
||||
Utilities.Log(LogLevel.INFO, $"{uc} users in database");
|
||||
if (uc == 0) Utilities.Log(LogLevel.WARN, "No users in database, first user will be promoted to admin");
|
||||
var db = new CollabVMAuthDbContext(context.Config.MySQL!.Configure().Options);
|
||||
// Make sure database schema is up-to-date, error if not
|
||||
if ((await db.Database.GetPendingMigrationsAsync()).Any()) {
|
||||
_logger.LogCritical("Database schema out of date. Please run migrations.");
|
||||
return 1;
|
||||
}
|
||||
// Count users in database
|
||||
var uc = await db.Users.CountAsync();
|
||||
_logger.LogInformation("{uc} users in database", uc);
|
||||
if (uc == 0) _logger.LogWarning("No users in database, first user will be promoted to admin");
|
||||
// Init cron
|
||||
var cron = new Cron(context.Config.MySQL.Configure().Options);
|
||||
await cron.Start();
|
||||
// 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");
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
_logger.LogCritical("Email verification is required but SMTP is disabled");
|
||||
return 1;
|
||||
}
|
||||
Mailer = Config.SMTP.Enabled ? new Mailer(Config.SMTP) : null;
|
||||
// Create hCaptcha client
|
||||
if (Config.hCaptcha.Enabled)
|
||||
if (Config.hCaptcha!.Enabled)
|
||||
{
|
||||
hCaptcha = new hCaptchaClient(Config.hCaptcha.Secret!, Config.hCaptcha.SiteKey!);
|
||||
Utilities.Log(LogLevel.INFO, "hCaptcha enabled");
|
||||
_logger.LogInformation("hCaptcha enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
Utilities.Log(LogLevel.INFO, "hCaptcha disabled");
|
||||
_logger.LogInformation("hCaptcha disabled");
|
||||
}
|
||||
// load password list
|
||||
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
||||
// Configure web server
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
#if DEBUG
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug);
|
||||
#else
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Warning);
|
||||
#endif
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
Utilities.ConfigureLogging(builder.Logging);
|
||||
|
||||
// Configure json serialization
|
||||
builder.Services.AddControllers().AddJsonOptions((options) => {
|
||||
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 =>
|
||||
{
|
||||
k.Listen(IPAddress.Parse(Config.HTTP.Host), Config.HTTP.Port);
|
||||
k.Listen(IPAddress.Parse(Config.HTTP!.Host!), Config.HTTP.Port);
|
||||
});
|
||||
builder.Services.AddCors();
|
||||
var app = builder.Build();
|
||||
if (context.Config.HTTP!.UseXForwardedFor) {
|
||||
app.UseForwardedHeaders();
|
||||
}
|
||||
app.UseRouting();
|
||||
// TODO: Make this more strict
|
||||
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
|
||||
Routes.RegisterRoutes(app);
|
||||
AdminRoutes.RegisterRoutes(app);
|
||||
DeveloperRoutes.RegisterRoutes(app);
|
||||
app.Run();
|
||||
app.MapControllers();
|
||||
await app.RunAsync();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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,28 +0,0 @@
|
||||
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,71 +1,20 @@
|
||||
using System.Net;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
FATAL
|
||||
}
|
||||
|
||||
|
||||
public static class Utilities
|
||||
{
|
||||
public static JsonSerializerOptions JsonSerializerOptions => new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
public static void Log(LogLevel level, string msg)
|
||||
{
|
||||
#if !DEBUG
|
||||
if (level == LogLevel.DEBUG)
|
||||
return;
|
||||
public static void ConfigureLogging(ILoggingBuilder builder) {
|
||||
builder.ClearProviders();
|
||||
builder.AddConsole();
|
||||
#if DEBUG
|
||||
builder.SetMinimumLevel(LogLevel.Debug);
|
||||
#else
|
||||
builder.SetMinimumLevel(LogLevel.Warning);
|
||||
#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)
|
||||
@@ -97,32 +46,4 @@ public static class Utilities
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
@@ -33,8 +36,8 @@ public class hCaptchaClient
|
||||
public class hCaptchaResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string challenge_ts { get; set; }
|
||||
public string hostname { get; set; }
|
||||
public string? challenge_ts { get; set; }
|
||||
public string? hostname { get; set; }
|
||||
public bool? credit { get; set; }
|
||||
[JsonPropertyName("error-codes")]
|
||||
public string[]? error_codes { get; set; }
|
||||
|
||||
14
README.MD
14
README.MD
@@ -7,12 +7,22 @@ This is the authentication server for CollabVM. It is used alongside CollabVM se
|
||||
- An SMTP server if you want email verification and password reset
|
||||
- An hCaptcha account if you want to use hCaptcha to prevent bots
|
||||
|
||||
## Running the server
|
||||
## Building the server
|
||||
1. Clone the source code: `git clone https://git.computernewb.com/collabvm/CollabVMAuthServer --recursive`
|
||||
2. Copy `config.example.toml` to `config.toml` and edit it to your liking
|
||||
3. Install dependencies: `dotnet restore`
|
||||
4. Build the server: `dotnet publish CollabVMAuthServer/CollabVMAuthServer.csproj -c Release --os linux -p:PublishReadyToRun=true`
|
||||
5. Run the server: `./CollabVMAuthServer/bin/Release/net8.0/linux-x64/publish/CollabVMAuthServer`
|
||||
|
||||
## Run Database Migrations
|
||||
You must do this the first time you run the server, and every time the database schema is updated (you will receive an error otherwise)
|
||||
```
|
||||
./CollabVMAuthServer/bin/Release/net8.0/linux-x64/publish/CollabVMAuthServer migrate-db
|
||||
```
|
||||
|
||||
## Running the server
|
||||
```
|
||||
./CollabVMAuthServer/bin/Release/net8.0/linux-x64/publish/CollabVMAuthServer
|
||||
```
|
||||
|
||||
## Setting up NGINX
|
||||
You'll want to set up NGINX as a reverse proxy to the authentication server to add HTTPS support. Running the server over plain HTTP is strongly discouraged. Here is an example NGINX configuration:
|
||||
|
||||
Reference in New Issue
Block a user