Add admin panel authentication and login system

Features:
- Login page with username/email + password
- Registration page for new accounts
- Logout functionality
- Cookie-based authentication (30-day sessions)
- Auto-redirect to login for unauthorized access
- User dropdown in navbar with logout link

Security:
- All admin pages now require authentication
- [Authorize] attribute on all admin PageModels
- Redirect to /Login if not authenticated
- Auto-login after registration

UI:
- Beautiful gradient login/register pages
- Consistent styling with admin panel
- User info displayed in navbar
- Logout link in dropdown menu

Starting resources for new users:
- 100,000 Gold
- 500,000 Cash
- Level 1
- Full admin panel access

Ready for production deployment!
This commit is contained in:
2026-02-19 15:06:08 -08:00
parent a6bab92282
commit e03c1d9856
15 changed files with 639 additions and 3 deletions

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class AdminModel : PageModel public class AdminModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class CatalogModel : PageModel public class CatalogModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using RR3CommunityServer.Models; using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class DeviceSettingsModel : PageModel public class DeviceSettingsModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -0,0 +1,163 @@
@page
@model RR3CommunityServer.Pages.LoginModel
@{
ViewData["Title"] = "Login";
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - RR3 Community Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
width: 100%;
max-width: 400px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
color: #667eea;
font-size: 28px;
margin-bottom: 5px;
}
.logo p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.error-message {
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
padding: 12px;
color: #c33;
margin-bottom: 20px;
font-size: 14px;
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-login:active {
transform: translateY(0);
}
.register-link {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
.register-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<h1>🏎️ RR3 Community Server</h1>
<p>Admin Panel Login</p>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="error-message">
@Model.ErrorMessage
</div>
}
<form method="post">
<div class="form-group">
<label for="Username">Username or Email</label>
<input type="text" id="Username" name="Username" required autofocus />
</div>
<div class="form-group">
<label for="Password">Password</label>
<input type="password" id="Password" name="Password" required />
</div>
<button type="submit" class="btn-login">Login</button>
</form>
<div class="register-link">
Don't have an account? <a asp-page="/Register">Register here</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using RR3CommunityServer.Services;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages;
public class LoginModel : PageModel
{
private readonly IAuthService _authService;
private readonly ILogger<LoginModel> _logger;
public LoginModel(IAuthService authService, ILogger<LoginModel> logger)
{
_authService = authService;
_logger = logger;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
public string? ErrorMessage { get; set; }
public void OnGet()
{
// If already logged in, redirect to admin panel
if (User.Identity?.IsAuthenticated == true)
{
Response.Redirect("/admin");
}
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = "Username and password are required";
return Page();
}
var loginRequest = new LoginRequest
{
UsernameOrEmail = Username,
Password = Password
};
var (success, response, error) = await _authService.LoginAsync(loginRequest);
if (!success || response == null)
{
ErrorMessage = error ?? "Invalid username or password";
_logger.LogWarning("Failed login attempt for: {Username}", Username);
return Page();
}
// Create authentication cookie
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, response.AccountId.ToString()),
new Claim(ClaimTypes.Name, response.Username),
new Claim(ClaimTypes.Email, response.Email)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true, // Remember me
ExpiresUtc = response.ExpiresAt
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
_logger.LogInformation("User logged in to admin panel: {Username}", response.Username);
return RedirectToPage("/Admin");
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace RR3CommunityServer.Pages;
public class LogoutModel : PageModel
{
private readonly ILogger<LogoutModel> _logger;
public LogoutModel(ILogger<LogoutModel> logger)
{
_logger = logger;
}
public async Task<IActionResult> OnGetAsync()
{
var username = User.Identity?.Name ?? "Unknown";
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
_logger.LogInformation("User logged out: {Username}", username);
return RedirectToPage("/Login");
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class PurchasesModel : PageModel public class PurchasesModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -0,0 +1,208 @@
@page
@model RR3CommunityServer.Pages.RegisterModel
@{
ViewData["Title"] = "Register";
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Register - RR3 Community Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.register-container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
width: 100%;
max-width: 450px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
color: #667eea;
font-size: 28px;
margin-bottom: 5px;
}
.logo p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.error-message {
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
padding: 12px;
color: #c33;
margin-bottom: 20px;
font-size: 14px;
}
.success-message {
background: #efe;
border: 1px solid #cfc;
border-radius: 6px;
padding: 12px;
color: #363;
margin-bottom: 20px;
font-size: 14px;
}
.btn-register {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-register:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-register:active {
transform: translateY(0);
}
.login-link {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
.login-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.login-link a:hover {
text-decoration: underline;
}
.info-box {
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 6px;
padding: 12px;
margin-bottom: 20px;
font-size: 13px;
color: #1976d2;
}
</style>
</head>
<body>
<div class="register-container">
<div class="logo">
<h1>🏎️ RR3 Community Server</h1>
<p>Create Account</p>
</div>
<div class="info-box">
<strong>Starting Resources:</strong><br>
• 100,000 Gold<br>
• 500,000 Cash<br>
• Access to admin panel
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="error-message">
@Model.ErrorMessage
</div>
}
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="success-message">
@Model.SuccessMessage
</div>
}
<form method="post">
<div class="form-group">
<label for="Username">Username</label>
<input type="text" id="Username" name="Username" required autofocus minlength="3" />
</div>
<div class="form-group">
<label for="Email">Email</label>
<input type="email" id="Email" name="Email" required />
</div>
<div class="form-group">
<label for="Password">Password</label>
<input type="password" id="Password" name="Password" required minlength="6" />
</div>
<div class="form-group">
<label for="ConfirmPassword">Confirm Password</label>
<input type="password" id="ConfirmPassword" name="ConfirmPassword" required minlength="6" />
</div>
<button type="submit" class="btn-register">Create Account</button>
</form>
<div class="login-link">
Already have an account? <a asp-page="/Login">Login here</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,110 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using RR3CommunityServer.Services;
using RR3CommunityServer.Models;
namespace RR3CommunityServer.Pages;
public class RegisterModel : PageModel
{
private readonly IAuthService _authService;
private readonly ILogger<RegisterModel> _logger;
public RegisterModel(IAuthService authService, ILogger<RegisterModel> logger)
{
_authService = authService;
_logger = logger;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Email { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
[BindProperty]
public string ConfirmPassword { get; set; } = string.Empty;
public string? ErrorMessage { get; set; }
public string? SuccessMessage { get; set; }
public void OnGet()
{
// If already logged in, redirect to admin panel
if (User.Identity?.IsAuthenticated == true)
{
Response.Redirect("/admin");
}
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Email) ||
string.IsNullOrWhiteSpace(Password) || string.IsNullOrWhiteSpace(ConfirmPassword))
{
ErrorMessage = "All fields are required";
return Page();
}
var registerRequest = new RegisterRequest
{
Username = Username,
Email = Email,
Password = Password,
ConfirmPassword = ConfirmPassword
};
var (success, token, error) = await _authService.RegisterAsync(registerRequest);
if (!success || string.IsNullOrEmpty(token))
{
ErrorMessage = error ?? "Registration failed";
_logger.LogWarning("Failed registration attempt for: {Username}", Username);
return Page();
}
_logger.LogInformation("New account registered: {Username} ({Email})", Username, Email);
// Auto-login after registration
var loginRequest = new LoginRequest
{
UsernameOrEmail = Username,
Password = Password
};
var (loginSuccess, response, loginError) = await _authService.LoginAsync(loginRequest);
if (loginSuccess && response != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, response.AccountId.ToString()),
new Claim(ClaimTypes.Name, response.Username),
new Claim(ClaimTypes.Email, response.Email)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = response.ExpiresAt
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return RedirectToPage("/Admin");
}
SuccessMessage = "Account created successfully! Please login.";
return RedirectToPage("/Login");
}
}

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class RewardsModel : PageModel public class RewardsModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class SessionsModel : PageModel public class SessionsModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class SettingsModel : PageModel public class SettingsModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using static RR3CommunityServer.Data.RR3DbContext; using static RR3CommunityServer.Data.RR3DbContext;
namespace RR3CommunityServer.Pages; namespace RR3CommunityServer.Pages;
[Authorize]
public class UsersModel : PageModel public class UsersModel : PageModel
{ {
private readonly RR3DbContext _context; private readonly RR3DbContext _context;

View File

@@ -108,6 +108,14 @@
<i class="bi bi-code-slash"></i> API <i class="bi bi-code-slash"></i> API
</a> </a>
</li> </li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> @User.Identity?.Name
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/Logout"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.Cookies;
using RR3CommunityServer.Data; using RR3CommunityServer.Data;
using RR3CommunityServer.Services; using RR3CommunityServer.Services;
using RR3CommunityServer.Middleware; using RR3CommunityServer.Middleware;
@@ -8,6 +9,20 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container // Add services to the container
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddRazorPages(); // Add Razor Pages support builder.Services.AddRazorPages(); // Add Razor Pages support
// Add cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.AccessDeniedPath = "/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
@@ -53,16 +68,19 @@ using (var scope = app.Services.CreateScope())
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseCors(); app.UseCors();
// Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();
// Custom middleware // Custom middleware
app.UseMiddleware<SynergyHeadersMiddleware>(); app.UseMiddleware<SynergyHeadersMiddleware>();
app.UseMiddleware<SessionValidationMiddleware>(); app.UseMiddleware<SessionValidationMiddleware>();
app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapRazorPages(); // Add Razor Pages routing app.MapRazorPages(); // Add Razor Pages routing
// Redirect root to admin panel // Redirect root to login page
app.MapGet("/", () => Results.Redirect("/admin")); app.MapGet("/", () => Results.Redirect("/Login"));
Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Real Racing 3 Community Server - RUNNING ║"); Console.WriteLine("║ Real Racing 3 Community Server - RUNNING ║");