Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.Auth;
using RadicadeServer.DB;
using RadicadeServer.Utils;
namespace RadicadeServer.Admin;
[Route("admin/auth")]
[EnableCors("AdminPanel")]
public class AdminAuthAPI(RecDBContext db) : Controller
{
private CookieOptions SessionCookieOptions => new()
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
MaxAge = AdminSession.Lifetime,
Path = "/"
};
public record LoginRequest(string? Username, string? Password);
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var username = request.Username?.Trim() ?? "";
var ip = Request.GetClientIp();
if (AdminLoginThrottle.IsLocked(username, ip, out var remaining))
return LockoutResponse(remaining);
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
return Unauthorized(new { error = "invalid_credentials", message = "Invalid username or password." });
var user = await db.AdminUsers.FirstOrDefaultAsync(x => x.Username == username);
if (user == null || !AdminSecurity.VerifyPassword(user.PasswordHash, request.Password))
{
await Task.Delay(350);
AdminLoginThrottle.RecordFailure(username, ip);
if (AdminLoginThrottle.IsLocked(username, ip, out remaining))
return LockoutResponse(remaining);
return Unauthorized(new { error = "invalid_credentials", message = "Invalid username or password." });
}
if (AdminSecurity.NeedsRehash(user.PasswordHash))
{
user.PasswordHash = AdminSecurity.HashPassword(request.Password);
await db.SaveChangesAsync();
}
AdminLoginThrottle.Clear(username, ip);
await AdminSession.PruneAsync(db);
return await CompleteLoginAsync(user);
}
private async Task<IActionResult> CompleteLoginAsync(AdminUser user)
{
var token = await AdminSession.CreateAsync(db, user.Id);
user.LastLoginAt = DateTime.UtcNow;
await db.SaveChangesAsync();
Response.Cookies.Append(AdminSession.CookieName, token, SessionCookieOptions);
return Ok(new { ok = true, user = Identity(user) });
}
[HttpGet("session")]
public async Task<IActionResult> Session()
{
var user = await RequireSessionAsync();
if (user == null) return Unauthorized(new { error = "not_authenticated" });
var (rotated, didRotate) = AdminSession.TryRotate(db, CurrentToken());
if (didRotate && !string.IsNullOrEmpty(rotated))
Response.Cookies.Append(AdminSession.CookieName, rotated, SessionCookieOptions);
return Ok(new { user = Identity(user) });
}
[HttpGet("me")]
public async Task<IActionResult> Me()
{
var user = await RequireSessionAsync();
if (user == null) return Unauthorized(new { error = "not_authenticated" });
return Ok(new { user = Identity(user) });
}
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
await AdminSession.RevokeAsync(db, CurrentToken());
Response.Cookies.Delete(AdminSession.CookieName, new CookieOptions { Path = "/" });
return Ok(new { ok = true });
}
private string? CurrentToken() =>
Request.Cookies.TryGetValue(AdminSession.CookieName, out var token) ? token : null;
private async Task<AdminUser?> RequireSessionAsync()
{
var (ok, userId) = await AdminSession.ValidateAsync(db, CurrentToken());
if (!ok) return null;
return await db.AdminUsers.FirstOrDefaultAsync(x => x.Id == userId);
}
private static object Identity(AdminUser user) => new
{
id = user.Id,
username = user.Username
};
private static string Display(string ip) => string.IsNullOrWhiteSpace(ip) ? "an unknown address" : ip;
private IActionResult LockoutResponse(TimeSpan remaining)
{
var seconds = (int)Math.Ceiling(remaining.TotalSeconds);
Response.Headers["Retry-After"] = seconds.ToString();
var message = seconds > 60
? $"Too many failed sign-in attempts. Try again in {(int)Math.Ceiling(remaining.TotalMinutes)} minutes."
: $"Too many failed sign-in attempts. Try again in {seconds} seconds.";
return StatusCode(429, new { error = "locked_out", message, retry_after_seconds = seconds });
}
}