mirror of
https://github.com/Stone-Red-Code/EchoHub.git
synced 2026-09-04 09:06:07 +02:00
- Added AvatarPath to ProfileEditResult for user profile updates. - Updated ProfileEditDialog to include avatar selection with a browse button. - Increased dialog height to accommodate new avatar input fields. - Implemented avatar file path handling in the profile edit dialog. feat: introduce moderation features and user roles - Added ServerRole enum to define user roles (Member, Mod, Admin, Owner). - Extended User model to include role, mute, and ban status. - Created ModerationController for user role assignment, kicking, banning, and muting. - Implemented methods in IChatBroadcaster and SignalRBroadcaster for user moderation actions. - Updated database schema with new columns for user roles and moderation states. fix: ensure muted users cannot send messages - Added mute status checks in ChatService to prevent message sending for muted users. - Updated user presence and status handling to reflect role changes and moderation actions. chore: update constants for ASCII art rendering - Introduced AsciiArtHeightHalfBlock constant for improved ASCII art rendering.
162 lines
6.3 KiB
C#
162 lines
6.3 KiB
C#
using EchoHub.Core.Constants;
|
|
using EchoHub.Core.DTOs;
|
|
using EchoHub.Core.Models;
|
|
using EchoHub.Server.Auth;
|
|
using EchoHub.Server.Data;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace EchoHub.Server.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/auth")]
|
|
[EnableRateLimiting("auth")]
|
|
public class AuthController : ControllerBase
|
|
{
|
|
private readonly EchoHubDbContext _db;
|
|
private readonly JwtTokenService _jwt;
|
|
|
|
public AuthController(EchoHubDbContext db, JwtTokenService jwt)
|
|
{
|
|
_db = db;
|
|
_jwt = jwt;
|
|
}
|
|
[HttpPost("register")]
|
|
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
|
return BadRequest(new ErrorResponse("Username and password are required."));
|
|
|
|
if (!ValidationConstants.UsernameRegex().IsMatch(request.Username))
|
|
return BadRequest(new ErrorResponse("Username must be 3-50 characters and contain only letters, digits, underscores, or hyphens."));
|
|
|
|
if (request.Password.Length < 6)
|
|
return BadRequest(new ErrorResponse("Password must be at least 6 characters."));
|
|
|
|
if (request.Password.Length > ValidationConstants.MaxPasswordLength)
|
|
return BadRequest(new ErrorResponse($"Password must not exceed {ValidationConstants.MaxPasswordLength} characters."));
|
|
|
|
var normalizedUsername = request.Username.ToLowerInvariant().Trim();
|
|
|
|
if (await _db.Users.AnyAsync(u => u.Username == normalizedUsername))
|
|
return Conflict(new ErrorResponse("Username is already taken."));
|
|
|
|
// First registered user on the server becomes the Owner
|
|
var isFirstUser = !await _db.Users.AnyAsync();
|
|
|
|
var user = new User
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Username = normalizedUsername,
|
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
|
|
DisplayName = request.DisplayName?.Trim(),
|
|
Role = isFirstUser ? ServerRole.Owner : ServerRole.Member,
|
|
};
|
|
|
|
_db.Users.Add(user);
|
|
await _db.SaveChangesAsync();
|
|
|
|
var (accessToken, expiresAt) = _jwt.GenerateAccessToken(user);
|
|
var refreshToken = JwtTokenService.GenerateRefreshToken();
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TokenHash = JwtTokenService.HashToken(refreshToken),
|
|
UserId = user.Id,
|
|
ExpiresAt = DateTimeOffset.UtcNow.Add(JwtTokenService.RefreshTokenLifetime),
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new LoginResponse(accessToken, refreshToken, expiresAt, user.Username, user.DisplayName, user.NicknameColor));
|
|
}
|
|
|
|
[HttpPost("login")]
|
|
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
|
return BadRequest(new ErrorResponse("Username and password are required."));
|
|
|
|
var normalizedUsername = request.Username.ToLowerInvariant().Trim();
|
|
var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == normalizedUsername);
|
|
|
|
if (user is null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
|
|
return Unauthorized(new ErrorResponse("Invalid username or password."));
|
|
|
|
if (user.IsBanned)
|
|
return Unauthorized(new ErrorResponse("Your account has been banned."));
|
|
|
|
user.LastSeenAt = DateTimeOffset.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
|
|
var (accessToken, expiresAt) = _jwt.GenerateAccessToken(user);
|
|
var refreshToken = JwtTokenService.GenerateRefreshToken();
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TokenHash = JwtTokenService.HashToken(refreshToken),
|
|
UserId = user.Id,
|
|
ExpiresAt = DateTimeOffset.UtcNow.Add(JwtTokenService.RefreshTokenLifetime),
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new LoginResponse(accessToken, refreshToken, expiresAt, user.Username, user.DisplayName, user.NicknameColor));
|
|
}
|
|
|
|
[HttpPost("refresh")]
|
|
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.RefreshToken))
|
|
return BadRequest(new ErrorResponse("Refresh token is required."));
|
|
|
|
var tokenHash = JwtTokenService.HashToken(request.RefreshToken);
|
|
var storedToken = await _db.RefreshTokens
|
|
.Include(r => r.User)
|
|
.FirstOrDefaultAsync(r => r.TokenHash == tokenHash);
|
|
|
|
if (storedToken is null || !storedToken.IsActive || storedToken.User is null)
|
|
return Unauthorized(new ErrorResponse("Invalid or expired refresh token."));
|
|
|
|
// Revoke old refresh token (rotation)
|
|
storedToken.RevokedAt = DateTimeOffset.UtcNow;
|
|
|
|
var user = storedToken.User;
|
|
user.LastSeenAt = DateTimeOffset.UtcNow;
|
|
|
|
// Issue new token pair
|
|
var (accessToken, expiresAt) = _jwt.GenerateAccessToken(user);
|
|
var newRefreshToken = JwtTokenService.GenerateRefreshToken();
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TokenHash = JwtTokenService.HashToken(newRefreshToken),
|
|
UserId = user.Id,
|
|
ExpiresAt = DateTimeOffset.UtcNow.Add(JwtTokenService.RefreshTokenLifetime),
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new LoginResponse(accessToken, newRefreshToken, expiresAt, user.Username, user.DisplayName, user.NicknameColor));
|
|
}
|
|
|
|
[HttpPost("logout")]
|
|
public async Task<IActionResult> Logout([FromBody] RefreshRequest request)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.RefreshToken))
|
|
return BadRequest(new ErrorResponse("Refresh token is required."));
|
|
|
|
var tokenHash = JwtTokenService.HashToken(request.RefreshToken);
|
|
var storedToken = await _db.RefreshTokens.FirstOrDefaultAsync(r => r.TokenHash == tokenHash);
|
|
|
|
if (storedToken is not null && storedToken.IsActive)
|
|
{
|
|
storedToken.RevokedAt = DateTimeOffset.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
return Ok();
|
|
}
|
|
}
|