mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user