implement a bunch more routes and functionality
This commit is contained in:
parent
16acd9b772
commit
d8ba680d34
19 changed files with 736 additions and 18 deletions
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using Isopoh.Cryptography.Argon2;
|
||||
using MySqlConnector;
|
||||
|
||||
|
@ -31,8 +32,9 @@ public class Database
|
|||
email TEXT NOT NULL UNIQUE KEY,
|
||||
email_verified BOOLEAN NOT NULL DEFAULT 0,
|
||||
email_verification_code CHAR(8) DEFAULT NULL,
|
||||
cvm_rank INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
banned BOOLEAN NOT NULL DEFAULT 0
|
||||
cvm_rank INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
banned BOOLEAN NOT NULL DEFAULT 0,
|
||||
registration_ip VARBINARY(16) NOT NULL
|
||||
);
|
||||
""";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
|
@ -42,6 +44,7 @@ public class Database
|
|||
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
|
||||
)
|
||||
""";
|
||||
|
@ -77,11 +80,12 @@ public class Database
|
|||
EmailVerified = reader.GetBoolean("email_verified"),
|
||||
EmailVerificationCode = reader.GetString("email_verification_code"),
|
||||
Rank = (Rank)reader.GetUInt32("cvm_rank"),
|
||||
Banned = reader.GetBoolean("banned")
|
||||
Banned = reader.GetBoolean("banned"),
|
||||
RegistrationIP = new IPAddress(await reader.GetFieldValueAsync<byte[]>(8))
|
||||
};
|
||||
}
|
||||
|
||||
public async Task RegisterAccount(string username, string email, string password, bool verified,
|
||||
public async Task RegisterAccount(string username, string email, string password, bool verified, IPAddress ip,
|
||||
string? verificationcode = null)
|
||||
{
|
||||
await using var db = new MySqlConnection(connectionString);
|
||||
|
@ -89,15 +93,16 @@ public class Database
|
|||
await using var cmd = db.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO users
|
||||
(username, password, email, email_verified, email_verification_code)
|
||||
(username, password, email, email_verified, email_verification_code, registration_ip)
|
||||
VALUES
|
||||
(@username, @password, @email, @email_verified, @email_verification_code)
|
||||
(@username, @password, @email, @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.AddWithValue("@email_verified", verified);
|
||||
cmd.Parameters.AddWithValue("@email_verification_code", verificationcode);
|
||||
cmd.Parameters.AddWithValue("@registration_ip", ip.GetAddressBytes());
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
|
@ -111,4 +116,127 @@ public class Database
|
|||
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(await reader.GetFieldValueAsync<byte[]>(4))
|
||||
});
|
||||
}
|
||||
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(await reader.GetFieldValueAsync<byte[]>(4))
|
||||
};
|
||||
}
|
||||
|
||||
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, string? newPassword, string? newEmail)
|
||||
{
|
||||
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);
|
||||
}
|
||||
cmd.CommandText = $"UPDATE users SET {string.Join(", ", updates)} WHERE username = @username";
|
||||
cmd.Parameters.AddWithValue("@username", username);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
|
@ -3,10 +3,13 @@ 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 class RegistrationConfig
|
||||
|
@ -15,10 +18,24 @@ public class RegistrationConfig
|
|||
public bool EmailDomainWhitelist { get; set; }
|
||||
public string[] AllowedEmailDomains { get; set; }
|
||||
}
|
||||
|
||||
public class AccountConfig
|
||||
{
|
||||
public int MaxSessions { get; set; }
|
||||
public int SessionExpiryDays { get; set; }
|
||||
}
|
||||
|
||||
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 class HTTPConfig
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool UseXForwardedFor { get; set; }
|
||||
public string[] TrustedProxies { get; set; }
|
||||
}
|
||||
public class MySQLConfig
|
||||
{
|
||||
|
|
8
CollabVMAuthServer/JoinPayload.cs
Normal file
8
CollabVMAuthServer/JoinPayload.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class JoinPayload
|
||||
{
|
||||
public string secretKey { get; set; }
|
||||
public string sessionToken { get; set; }
|
||||
public string ip { get; set; }
|
||||
}
|
10
CollabVMAuthServer/JoinResponse.cs
Normal file
10
CollabVMAuthServer/JoinResponse.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class JoinResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public bool clientSuccess { get; set; } = false;
|
||||
public string? error { get; set; }
|
||||
public string? username { get; set; }
|
||||
public Rank? rank { get; set; }
|
||||
}
|
8
CollabVMAuthServer/LoginPayload.cs
Normal file
8
CollabVMAuthServer/LoginPayload.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class LoginPayload
|
||||
{
|
||||
public string username { get; set; }
|
||||
public string password { get; set; }
|
||||
public string? captchaToken { get; set; }
|
||||
}
|
11
CollabVMAuthServer/LoginResponse.cs
Normal file
11
CollabVMAuthServer/LoginResponse.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
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; }
|
||||
}
|
6
CollabVMAuthServer/LogoutPayload.cs
Normal file
6
CollabVMAuthServer/LogoutPayload.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class LogoutPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
}
|
7
CollabVMAuthServer/LogoutResponse.cs
Normal file
7
CollabVMAuthServer/LogoutResponse.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class LogoutResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
}
|
|
@ -56,14 +56,20 @@ public class Program
|
|||
BannedPasswords = await File.ReadAllLinesAsync("rockyou.txt");
|
||||
// Configure web server
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
#if !DEBUG
|
||||
#if DEBUG
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Debug);
|
||||
#else
|
||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Warning);
|
||||
#endif
|
||||
builder.WebHost.UseKestrel(k =>
|
||||
{
|
||||
k.Listen(IPAddress.Parse(Config.HTTP.Host), Config.HTTP.Port);
|
||||
});
|
||||
builder.Services.AddCors();
|
||||
var app = builder.Build();
|
||||
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}"));
|
||||
// Register routes
|
||||
Routes.RegisterRoutes(app);
|
||||
|
|
|
@ -7,4 +7,5 @@ public class RegisterResponse
|
|||
public bool? verificationRequired { get; set; } = null;
|
||||
public string? username { get; set; }
|
||||
public string? email { get; set; }
|
||||
public string? sessionToken { get; set; }
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Isopoh.Cryptography.Argon2;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
|
@ -12,6 +14,415 @@ public static class Routes
|
|||
app.MapGet("/api/v1/info", HandleInfo);
|
||||
app.MapPost("/api/v1/register", (Delegate) HandleRegister);
|
||||
app.MapPost("/api/v1/verify", (Delegate) HandleVerify);
|
||||
app.MapPost("/api/v1/login", (Delegate) HandleLogin);
|
||||
app.MapPost("/api/v1/session", (Delegate) HandleSession);
|
||||
app.MapPost("/api/v1/join", (Delegate)HandleJoin);
|
||||
app.MapPost("/api/v1/logout", (Delegate)HandleLogout);
|
||||
app.MapPost("/api/v1/update", (Delegate)HandleUpdate);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleUpdate(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var payload = await context.Request.ReadFromJsonAsync<UpdatePayload>();
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token) ||
|
||||
string.IsNullOrWhiteSpace(payload.currentPassword) || (string.IsNullOrWhiteSpace(payload.newPassword) && string.IsNullOrWhiteSpace(payload.username) && string.IsNullOrWhiteSpace(payload.email)))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is valid
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null || DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays))
|
||||
{
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check password
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("User not found in database (something is very wrong)");
|
||||
if (!Argon2.Verify(user.Password, payload.currentPassword))
|
||||
{
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid password",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Validate new username
|
||||
if (!string.IsNullOrWhiteSpace(payload.username) && !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."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Validate new E-Mail
|
||||
if (!string.IsNullOrWhiteSpace(payload.email) && !new EmailAddressAttribute().IsValid(payload.email))
|
||||
{
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Malformed E-Mail address."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(payload.email) && 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."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Make sure username isn't taken
|
||||
var _user = await Program.Database.GetUser(payload.username);
|
||||
if (_user != null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
{
|
||||
success = false,
|
||||
error = "That username is taken."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if E-Mail is in use
|
||||
_user = await Program.Database.GetUser(email: payload.email);
|
||||
if (_user != null)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
{
|
||||
success = false,
|
||||
error = "That E-Mail is already in use."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Validate new password
|
||||
if (!string.IsNullOrWhiteSpace(payload.newPassword))
|
||||
{
|
||||
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."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
if (Program.BannedPasswords.Contains(payload.newPassword))
|
||||
{
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "That password is commonly used and is not allowed."
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
// Check for duplicate changes
|
||||
if (payload.username == user.Username || payload.email == user.Email ||
|
||||
Argon2.Verify(user.Password, payload.newPassword))
|
||||
{
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = false,
|
||||
error = "No changes were made."
|
||||
});
|
||||
}
|
||||
// Perform update
|
||||
await Program.Database.UpdateUser(user.Username, payload.username, payload.newPassword, payload.email);
|
||||
// Revoke all sessions
|
||||
await Program.Database.RevokeAllSessions(user.Username);
|
||||
// Unverify the account if the E-Mail was changed
|
||||
if (payload.email != null)
|
||||
{
|
||||
await Program.Database.SetUserVerified(user.Username, false);
|
||||
var code = Program.Random.Next(10000000, 99999999).ToString();
|
||||
await Program.Database.SetVerificationCode(user.Username, code);
|
||||
await Program.Mailer.SendVerificationCode(user.Username, payload.email, code);
|
||||
}
|
||||
return Results.Json(new UpdateResponse
|
||||
{
|
||||
success = true,
|
||||
verificationRequired = payload.email != null,
|
||||
sessionExpired = true
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleLogout(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LogoutResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var payload = await context.Request.ReadFromJsonAsync<LogoutPayload>();
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LogoutResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is valid
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null)
|
||||
{
|
||||
return Results.Json(new LogoutResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Revoke session
|
||||
await Program.Database.RevokeSession(payload.token);
|
||||
return Results.Json(new LogoutResponse
|
||||
{
|
||||
success = true
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleSession(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new SessionResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var payload = await context.Request.ReadFromJsonAsync<SessionPayload>();
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.token))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new SessionResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is valid
|
||||
var session = await Program.Database.GetSession(payload.token);
|
||||
if (session == null)
|
||||
{
|
||||
return Results.Json(new SessionResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is expired
|
||||
if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays))
|
||||
{
|
||||
return Results.Json(new SessionResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Expired session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("User not found in database (something is very wrong)");
|
||||
return Results.Json(new SessionResponse
|
||||
{
|
||||
success = true,
|
||||
banned = user.Banned,
|
||||
username = user.Username,
|
||||
email = user.Email
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleJoin(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var payload = await context.Request.ReadFromJsonAsync<JoinPayload>();
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.secretKey) || string.IsNullOrWhiteSpace(payload.sessionToken) || string.IsNullOrWhiteSpace(payload.ip))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check secret key
|
||||
if (payload.secretKey != Program.Config.CollabVM.SecretKey)
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid secret key"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is valid
|
||||
var session = await Program.Database.GetSession(payload.sessionToken);
|
||||
if (session == null)
|
||||
{
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = true,
|
||||
clientSuccess = false,
|
||||
error = "Invalid session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if session is expired
|
||||
if (DateTime.Now > session.LastUsed.AddDays(Program.Config.Accounts.SessionExpiryDays))
|
||||
{
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = true,
|
||||
clientSuccess = false,
|
||||
error = "Invalid session",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if banned
|
||||
var user = await Program.Database.GetUser(session.Username)
|
||||
?? throw new Exception("User not found in database (something is very wrong)");
|
||||
if (user.Banned)
|
||||
{
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = true,
|
||||
clientSuccess = false,
|
||||
error = "You are banned",
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Update session
|
||||
await Program.Database.UpdateSessionLastUsed(session.Token, IPAddress.Parse(payload.ip));
|
||||
return Results.Json(new JoinResponse
|
||||
{
|
||||
success = true,
|
||||
clientSuccess = true,
|
||||
username = session.Username,
|
||||
rank = user.Rank
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleLogin(HttpContext context)
|
||||
{
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var payload = await context.Request.ReadFromJsonAsync<LoginPayload>();
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.username) || string.IsNullOrWhiteSpace(payload.password))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var ip = Utilities.GetIP(context);
|
||||
if (ip == null)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Empty;
|
||||
}
|
||||
// Check captcha response
|
||||
if (Program.Config.hCaptcha.Enabled)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload.captchaToken))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Missing hCaptcha token"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var result =
|
||||
await Program.hCaptcha!.Verify(payload.captchaToken, ip.ToString());
|
||||
if (!result.success)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid captcha response"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
// Validate username and password
|
||||
var user = await Program.Database.GetUser(payload.username);
|
||||
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid username or password"
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
// Check if account is verified
|
||||
if (!user.EmailVerified)
|
||||
{
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = true,
|
||||
verificationRequired = true,
|
||||
email = user.Email,
|
||||
username = user.Username,
|
||||
});
|
||||
}
|
||||
// Check max sessions
|
||||
var sessions = await Program.Database.GetSessions(user.Username);
|
||||
if (sessions.Length >= Program.Config.Accounts.MaxSessions)
|
||||
{
|
||||
var oldest = sessions.OrderBy(s => s.LastUsed).First();
|
||||
await Program.Database.RevokeSession(oldest.Token);
|
||||
}
|
||||
// Generate token
|
||||
var token = Utilities.RandomString(32);
|
||||
await Program.Database.CreateSession(user.Username, token, ip);
|
||||
return Results.Json(new LoginResponse
|
||||
{
|
||||
success = true,
|
||||
token = token,
|
||||
username = user.Username,
|
||||
email = user.Email
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleVerify(HttpContext context)
|
||||
|
@ -20,7 +431,7 @@ public static class Routes
|
|||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
|
@ -32,7 +443,7 @@ public static class Routes
|
|||
string.IsNullOrWhiteSpace(payload.password) || string.IsNullOrWhiteSpace(payload.password))
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid request body"
|
||||
|
@ -43,7 +454,7 @@ public static class Routes
|
|||
if (user == null || !Argon2.Verify(user.Password, payload.password))
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Json(new RegisterResponse
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid username or password"
|
||||
|
@ -53,7 +464,7 @@ public static class Routes
|
|||
if (user.EmailVerified)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Account is already verified"
|
||||
|
@ -63,7 +474,7 @@ public static class Routes
|
|||
if (user.EmailVerificationCode != payload.code)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return Results.Json(new RegisterResponse
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = false,
|
||||
error = "Invalid verification code"
|
||||
|
@ -71,14 +482,30 @@ public static class Routes
|
|||
}
|
||||
// Verify the account
|
||||
await Program.Database.SetUserVerified(payload.username, true);
|
||||
return Results.Json(new RegisterResponse
|
||||
// Create a session
|
||||
var token = Utilities.RandomString(32);
|
||||
var ip = Utilities.GetIP(context);
|
||||
if (ip == null)
|
||||
{
|
||||
success = true
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Empty;
|
||||
}
|
||||
await Program.Database.CreateSession(user.Username, token, ip);
|
||||
return Results.Json(new VerifyResponse
|
||||
{
|
||||
success = true,
|
||||
sessionToken = token,
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleRegister(HttpContext context)
|
||||
{
|
||||
var ip = Utilities.GetIP(context);
|
||||
if (ip == null)
|
||||
{
|
||||
context.Response.StatusCode = 403;
|
||||
return Results.Empty;
|
||||
}
|
||||
// Check payload
|
||||
if (context.Request.ContentType != "application/json")
|
||||
{
|
||||
|
@ -112,7 +539,7 @@ public static class Routes
|
|||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
var result =
|
||||
await Program.hCaptcha!.Verify(payload.captchaToken, context.Connection.RemoteIpAddress!.ToString());
|
||||
await Program.hCaptcha!.Verify(payload.captchaToken, ip.ToString());
|
||||
if (!result.success)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
|
@ -198,7 +625,7 @@ public static class Routes
|
|||
if (Program.Config.Registration.EmailVerificationRequired)
|
||||
{
|
||||
var code = Program.Random.Next(10000000, 99999999).ToString();
|
||||
await Program.Database.RegisterAccount(payload.username, payload.email, payload.password, false, code);
|
||||
await Program.Database.RegisterAccount(payload.username, payload.email, payload.password, false, ip,code);
|
||||
await Program.Mailer.SendVerificationCode(payload.username, payload.email, code);
|
||||
return Results.Json(new RegisterResponse
|
||||
{
|
||||
|
@ -211,12 +638,15 @@ public static class Routes
|
|||
else
|
||||
{
|
||||
await Program.Database.RegisterAccount(payload.username, payload.email, payload.password, true, null);
|
||||
var token = Utilities.RandomString(32);
|
||||
await Program.Database.CreateSession(user.Username, token, ip);
|
||||
return Results.Json(new RegisterResponse
|
||||
{
|
||||
success = true,
|
||||
verificationRequired = false,
|
||||
email = payload.email,
|
||||
username = payload.username
|
||||
username = payload.username,
|
||||
sessionToken = token
|
||||
}, Utilities.JsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class Session
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public uint UserId { get; set; }
|
||||
public string Username { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime LastUsed { get; set; }
|
||||
public IPAddress LastIP { get; set; }
|
||||
}
|
6
CollabVMAuthServer/SessionPayload.cs
Normal file
6
CollabVMAuthServer/SessionPayload.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class SessionPayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
}
|
10
CollabVMAuthServer/SessionResponse.cs
Normal file
10
CollabVMAuthServer/SessionResponse.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class SessionResponse
|
||||
{
|
||||
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; }
|
||||
}
|
11
CollabVMAuthServer/UpdatePayload.cs
Normal file
11
CollabVMAuthServer/UpdatePayload.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class UpdatePayload
|
||||
{
|
||||
public string token { get; set; }
|
||||
public string currentPassword { get; set; }
|
||||
|
||||
public string? newPassword { get; set; }
|
||||
public string? username { get; set; }
|
||||
public string? email { get; set; }
|
||||
}
|
9
CollabVMAuthServer/UpdateResponse.cs
Normal file
9
CollabVMAuthServer/UpdateResponse.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class UpdateResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public bool? verificationRequired { get; set; } = null;
|
||||
public bool? sessionExpired { get; set; } = null;
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class User
|
||||
|
@ -10,6 +12,7 @@ public class User
|
|||
public string EmailVerificationCode { get; set; }
|
||||
public Rank Rank { get; set; }
|
||||
public bool Banned { get; set; }
|
||||
public IPAddress RegistrationIP { get; set; }
|
||||
}
|
||||
|
||||
public enum Rank : uint
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
@ -84,4 +85,39 @@ public static class Utilities
|
|||
new Regex("[!@#$%^&*()\\-_=+\\\\|\\[\\];:'\\\",<.>/?`~]").IsMatch(password) &&
|
||||
new Regex("[0-9]").IsMatch(password);
|
||||
}
|
||||
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder str = new StringBuilder();
|
||||
Random rand = new Random();
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
str.Append(chars[rand.Next(chars.Length)]);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
8
CollabVMAuthServer/VerifyResponse.cs
Normal file
8
CollabVMAuthServer/VerifyResponse.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Computernewb.CollabVMAuthServer;
|
||||
|
||||
public class VerifyResponse
|
||||
{
|
||||
public bool success { get; set; }
|
||||
public string? error { get; set; }
|
||||
public string? sessionToken { get; set; }
|
||||
}
|
Loading…
Reference in a new issue