Add user authentication and account management system
Features:
- User registration with username, email, password
- Login with JWT token authentication
- Password hashing with BCrypt
- Account settings & management
- Device linking to accounts
- Change password & password reset
- Account-User relationship (1-to-1 with game data)
Database entities:
- Account: User accounts with credentials
- DeviceAccount: Link devices to accounts (many-to-many)
API endpoints:
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/change-password
- POST /api/auth/forgot-password
- POST /api/auth/reset-password
- GET /api/auth/me
- POST /api/auth/link-device
- DELETE /api/auth/unlink-device/{deviceId}
Starting resources for new accounts:
- 100,000 Gold
- 500,000 Cash
- Level 1
Ready for VPS deployment with HTTPS.
This commit is contained in:
128
RR3CommunityServer/Controllers/AuthController.cs
Normal file
128
RR3CommunityServer/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RR3CommunityServer.Models;
|
||||
using RR3CommunityServer.Services;
|
||||
|
||||
namespace RR3CommunityServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
|
||||
public AuthController(IAuthService authService, ILogger<AuthController> logger)
|
||||
{
|
||||
_authService = authService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
var (success, token, error) = await _authService.RegisterAsync(request);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Account created successfully", token });
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
var (success, response, error) = await _authService.LoginAsync(request);
|
||||
|
||||
if (!success)
|
||||
return Unauthorized(new { message = error });
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("change-password")]
|
||||
public async Task<ActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||
{
|
||||
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
||||
var account = await _authService.ValidateTokenAsync(token);
|
||||
|
||||
if (account == null)
|
||||
return Unauthorized(new { message = "Invalid or expired token" });
|
||||
|
||||
var (success, error) = await _authService.ChangePasswordAsync(account.Id, request);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Password changed successfully" });
|
||||
}
|
||||
|
||||
[HttpPost("forgot-password")]
|
||||
public async Task<ActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
|
||||
{
|
||||
var (success, error) = await _authService.ForgotPasswordAsync(request);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Password reset instructions sent to your email" });
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<ActionResult> ResetPassword([FromBody] ResetPasswordRequest request)
|
||||
{
|
||||
var (success, error) = await _authService.ResetPasswordAsync(request);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Password reset successfully" });
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult> GetCurrentUser()
|
||||
{
|
||||
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
||||
var account = await _authService.ValidateTokenAsync(token);
|
||||
|
||||
if (account == null)
|
||||
return Unauthorized(new { message = "Invalid or expired token" });
|
||||
|
||||
var settings = await _authService.GetAccountSettingsAsync(account.Id);
|
||||
|
||||
return Ok(settings);
|
||||
}
|
||||
|
||||
[HttpPost("link-device")]
|
||||
public async Task<ActionResult> LinkDevice([FromBody] LinkDeviceRequest request)
|
||||
{
|
||||
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
||||
var account = await _authService.ValidateTokenAsync(token);
|
||||
|
||||
if (account == null)
|
||||
return Unauthorized(new { message = "Invalid or expired token" });
|
||||
|
||||
var (success, error) = await _authService.LinkDeviceAsync(account.Id, request);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Device linked successfully" });
|
||||
}
|
||||
|
||||
[HttpDelete("unlink-device/{deviceId}")]
|
||||
public async Task<ActionResult> UnlinkDevice(string deviceId)
|
||||
{
|
||||
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
|
||||
var account = await _authService.ValidateTokenAsync(token);
|
||||
|
||||
if (account == null)
|
||||
return Unauthorized(new { message = "Invalid or expired token" });
|
||||
|
||||
var (success, error) = await _authService.UnlinkDeviceAsync(account.Id, deviceId);
|
||||
|
||||
if (!success)
|
||||
return BadRequest(new { message = error });
|
||||
|
||||
return Ok(new { message = "Device unlinked successfully" });
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ public class RR3DbContext : DbContext
|
||||
public DbSet<Device> Devices { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Session> Sessions { get; set; }
|
||||
public DbSet<Account> Accounts { get; set; }
|
||||
public DbSet<DeviceAccount> DeviceAccounts { get; set; }
|
||||
public DbSet<Purchase> Purchases { get; set; }
|
||||
public DbSet<CatalogItem> CatalogItems { get; set; }
|
||||
public DbSet<DailyReward> DailyRewards { get; set; }
|
||||
|
||||
113
RR3CommunityServer/Models/AccountModels.cs
Normal file
113
RR3CommunityServer/Models/AccountModels.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using RR3CommunityServer.Data;
|
||||
|
||||
namespace RR3CommunityServer.Models;
|
||||
|
||||
// Account entity with authentication
|
||||
public class Account
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? LastLoginAt { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool EmailVerified { get; set; } = false;
|
||||
public string? EmailVerificationToken { get; set; }
|
||||
public string? PasswordResetToken { get; set; }
|
||||
public DateTime? PasswordResetExpiry { get; set; }
|
||||
|
||||
// Link to game user data
|
||||
public int? UserId { get; set; }
|
||||
public User? User { get; set; }
|
||||
|
||||
// Multiple devices can be linked to one account
|
||||
public List<DeviceAccount> LinkedDevices { get; set; } = new();
|
||||
}
|
||||
|
||||
// Join table for account-device relationship
|
||||
public class DeviceAccount
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AccountId { get; set; }
|
||||
public Account Account { get; set; } = null!;
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string? DeviceName { get; set; }
|
||||
public DateTime LinkedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastUsedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Request/Response DTOs for authentication
|
||||
public class RegisterRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string UsernameOrEmail { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string? DeviceId { get; set; }
|
||||
}
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public int AccountId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
public class ChangePasswordRequest
|
||||
{
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ForgotPasswordRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ResetPasswordRequest
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LinkDeviceRequest
|
||||
{
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string? DeviceName { get; set; }
|
||||
}
|
||||
|
||||
public class AccountSettingsResponse
|
||||
{
|
||||
public int AccountId { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public bool EmailVerified { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? LastLoginAt { get; set; }
|
||||
public List<LinkedDeviceInfo> LinkedDevices { get; set; } = new();
|
||||
|
||||
// Game progress
|
||||
public int? Gold { get; set; }
|
||||
public int? Cash { get; set; }
|
||||
public int? Level { get; set; }
|
||||
public int? CarsOwned { get; set; }
|
||||
}
|
||||
|
||||
public class LinkedDeviceInfo
|
||||
{
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string? DeviceName { get; set; }
|
||||
public DateTime LinkedAt { get; set; }
|
||||
public DateTime LastUsedAt { get; set; }
|
||||
}
|
||||
@@ -20,6 +20,7 @@ builder.Services.AddScoped<ISessionService, SessionService>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<ICatalogService, CatalogService>();
|
||||
builder.Services.AddScoped<IDrmService, DrmService>();
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<AssetExtractionService>();
|
||||
|
||||
// CORS for cross-origin requests
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
380
RR3CommunityServer/Services/AuthService.cs
Normal file
380
RR3CommunityServer/Services/AuthService.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RR3CommunityServer.Data;
|
||||
using RR3CommunityServer.Models;
|
||||
|
||||
namespace RR3CommunityServer.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<(bool Success, string? Token, string? Error)> RegisterAsync(RegisterRequest request);
|
||||
Task<(bool Success, LoginResponse? Response, string? Error)> LoginAsync(LoginRequest request);
|
||||
Task<(bool Success, string? Error)> ChangePasswordAsync(int accountId, ChangePasswordRequest request);
|
||||
Task<(bool Success, string? Error)> ForgotPasswordAsync(ForgotPasswordRequest request);
|
||||
Task<(bool Success, string? Error)> ResetPasswordAsync(ResetPasswordRequest request);
|
||||
Task<(bool Success, string? Error)> LinkDeviceAsync(int accountId, LinkDeviceRequest request);
|
||||
Task<(bool Success, string? Error)> UnlinkDeviceAsync(int accountId, string deviceId);
|
||||
Task<AccountSettingsResponse?> GetAccountSettingsAsync(int accountId);
|
||||
Task<Account?> ValidateTokenAsync(string token);
|
||||
}
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly RR3DbContext _context;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
public AuthService(RR3DbContext context, IConfiguration configuration, ILogger<AuthService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Token, string? Error)> RegisterAsync(RegisterRequest request)
|
||||
{
|
||||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(request.Username) || request.Username.Length < 3)
|
||||
return (false, null, "Username must be at least 3 characters");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains('@'))
|
||||
return (false, null, "Invalid email address");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 6)
|
||||
return (false, null, "Password must be at least 6 characters");
|
||||
|
||||
if (request.Password != request.ConfirmPassword)
|
||||
return (false, null, "Passwords do not match");
|
||||
|
||||
// Check if username or email already exists
|
||||
var existingUsername = await _context.Set<Account>()
|
||||
.AnyAsync(a => a.Username.ToLower() == request.Username.ToLower());
|
||||
if (existingUsername)
|
||||
return (false, null, "Username already taken");
|
||||
|
||||
var existingEmail = await _context.Set<Account>()
|
||||
.AnyAsync(a => a.Email.ToLower() == request.Email.ToLower());
|
||||
if (existingEmail)
|
||||
return (false, null, "Email already registered");
|
||||
|
||||
// Create account
|
||||
var account = new Account
|
||||
{
|
||||
Username = request.Username,
|
||||
Email = request.Email,
|
||||
PasswordHash = HashPassword(request.Password),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
EmailVerified = false,
|
||||
EmailVerificationToken = GenerateToken()
|
||||
};
|
||||
|
||||
// Create associated game user
|
||||
var user = new User
|
||||
{
|
||||
SynergyId = Guid.NewGuid().ToString(),
|
||||
Nickname = request.Username,
|
||||
Gold = 100000, // Starting gold for community server
|
||||
Cash = 500000, // Starting cash
|
||||
Level = 1,
|
||||
Experience = 0,
|
||||
Reputation = 0,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
account.UserId = user.Id;
|
||||
_context.Set<Account>().Add(account);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("New account registered: {Username} ({Email})", account.Username, account.Email);
|
||||
|
||||
// Generate JWT token
|
||||
var token = GenerateJwtToken(account);
|
||||
return (true, token, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, LoginResponse? Response, string? Error)> LoginAsync(LoginRequest request)
|
||||
{
|
||||
// Find account by username or email
|
||||
var account = await _context.Set<Account>()
|
||||
.Include(a => a.User)
|
||||
.FirstOrDefaultAsync(a =>
|
||||
a.Username.ToLower() == request.UsernameOrEmail.ToLower() ||
|
||||
a.Email.ToLower() == request.UsernameOrEmail.ToLower());
|
||||
|
||||
if (account == null)
|
||||
return (false, null, "Invalid username/email or password");
|
||||
|
||||
if (!account.IsActive)
|
||||
return (false, null, "Account is disabled");
|
||||
|
||||
// Verify password
|
||||
if (!VerifyPassword(request.Password, account.PasswordHash))
|
||||
return (false, null, "Invalid username/email or password");
|
||||
|
||||
// Update last login
|
||||
account.LastLoginAt = DateTime.UtcNow;
|
||||
|
||||
// Link device if provided
|
||||
if (!string.IsNullOrEmpty(request.DeviceId))
|
||||
{
|
||||
var deviceLink = await _context.Set<DeviceAccount>()
|
||||
.FirstOrDefaultAsync(da => da.AccountId == account.Id && da.DeviceId == request.DeviceId);
|
||||
|
||||
if (deviceLink == null)
|
||||
{
|
||||
deviceLink = new DeviceAccount
|
||||
{
|
||||
AccountId = account.Id,
|
||||
DeviceId = request.DeviceId,
|
||||
LinkedAt = DateTime.UtcNow,
|
||||
LastUsedAt = DateTime.UtcNow
|
||||
};
|
||||
_context.Set<DeviceAccount>().Add(deviceLink);
|
||||
}
|
||||
else
|
||||
{
|
||||
deviceLink.LastUsedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("User logged in: {Username}", account.Username);
|
||||
|
||||
// Generate JWT token
|
||||
var token = GenerateJwtToken(account);
|
||||
var expiresAt = DateTime.UtcNow.AddDays(30);
|
||||
|
||||
var response = new LoginResponse
|
||||
{
|
||||
Token = token,
|
||||
AccountId = account.Id,
|
||||
Username = account.Username,
|
||||
Email = account.Email,
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
|
||||
return (true, response, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Error)> ChangePasswordAsync(int accountId, ChangePasswordRequest request)
|
||||
{
|
||||
var account = await _context.Set<Account>().FindAsync(accountId);
|
||||
if (account == null)
|
||||
return (false, "Account not found");
|
||||
|
||||
if (!VerifyPassword(request.CurrentPassword, account.PasswordHash))
|
||||
return (false, "Current password is incorrect");
|
||||
|
||||
if (request.NewPassword.Length < 6)
|
||||
return (false, "New password must be at least 6 characters");
|
||||
|
||||
if (request.NewPassword != request.ConfirmPassword)
|
||||
return (false, "Passwords do not match");
|
||||
|
||||
account.PasswordHash = HashPassword(request.NewPassword);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Password changed for account: {AccountId}", accountId);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Error)> ForgotPasswordAsync(ForgotPasswordRequest request)
|
||||
{
|
||||
var account = await _context.Set<Account>()
|
||||
.FirstOrDefaultAsync(a => a.Email.ToLower() == request.Email.ToLower());
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
// Don't reveal if email exists
|
||||
_logger.LogWarning("Password reset requested for non-existent email: {Email}", request.Email);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
account.PasswordResetToken = GenerateToken();
|
||||
account.PasswordResetExpiry = DateTime.UtcNow.AddHours(24);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Password reset token generated for: {Email}", request.Email);
|
||||
|
||||
// TODO: Send email with reset link
|
||||
// For now, just log the token
|
||||
_logger.LogWarning("Password reset token: {Token} (implement email service)", account.PasswordResetToken);
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Error)> ResetPasswordAsync(ResetPasswordRequest request)
|
||||
{
|
||||
var account = await _context.Set<Account>()
|
||||
.FirstOrDefaultAsync(a => a.PasswordResetToken == request.Token);
|
||||
|
||||
if (account == null || account.PasswordResetExpiry == null || account.PasswordResetExpiry < DateTime.UtcNow)
|
||||
return (false, "Invalid or expired reset token");
|
||||
|
||||
if (request.NewPassword.Length < 6)
|
||||
return (false, "Password must be at least 6 characters");
|
||||
|
||||
if (request.NewPassword != request.ConfirmPassword)
|
||||
return (false, "Passwords do not match");
|
||||
|
||||
account.PasswordHash = HashPassword(request.NewPassword);
|
||||
account.PasswordResetToken = null;
|
||||
account.PasswordResetExpiry = null;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Password reset completed for account: {AccountId}", account.Id);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Error)> LinkDeviceAsync(int accountId, LinkDeviceRequest request)
|
||||
{
|
||||
var account = await _context.Set<Account>().FindAsync(accountId);
|
||||
if (account == null)
|
||||
return (false, "Account not found");
|
||||
|
||||
var existingLink = await _context.Set<DeviceAccount>()
|
||||
.FirstOrDefaultAsync(da => da.AccountId == accountId && da.DeviceId == request.DeviceId);
|
||||
|
||||
if (existingLink != null)
|
||||
return (false, "Device already linked");
|
||||
|
||||
var deviceLink = new DeviceAccount
|
||||
{
|
||||
AccountId = accountId,
|
||||
DeviceId = request.DeviceId,
|
||||
DeviceName = request.DeviceName,
|
||||
LinkedAt = DateTime.UtcNow,
|
||||
LastUsedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Set<DeviceAccount>().Add(deviceLink);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Device {DeviceId} linked to account {AccountId}", request.DeviceId, accountId);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string? Error)> UnlinkDeviceAsync(int accountId, string deviceId)
|
||||
{
|
||||
var deviceLink = await _context.Set<DeviceAccount>()
|
||||
.FirstOrDefaultAsync(da => da.AccountId == accountId && da.DeviceId == deviceId);
|
||||
|
||||
if (deviceLink == null)
|
||||
return (false, "Device not linked to this account");
|
||||
|
||||
_context.Set<DeviceAccount>().Remove(deviceLink);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Device {DeviceId} unlinked from account {AccountId}", deviceId, accountId);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<AccountSettingsResponse?> GetAccountSettingsAsync(int accountId)
|
||||
{
|
||||
var account = await _context.Set<Account>()
|
||||
.Include(a => a.User)
|
||||
.Include(a => a.LinkedDevices)
|
||||
.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
if (account == null)
|
||||
return null;
|
||||
|
||||
var carsOwned = account.UserId.HasValue
|
||||
? await _context.OwnedCars.CountAsync(c => c.UserId == account.UserId.Value)
|
||||
: 0;
|
||||
|
||||
return new AccountSettingsResponse
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Username = account.Username,
|
||||
Email = account.Email,
|
||||
EmailVerified = account.EmailVerified,
|
||||
CreatedAt = account.CreatedAt,
|
||||
LastLoginAt = account.LastLoginAt,
|
||||
LinkedDevices = account.LinkedDevices.Select(d => new LinkedDeviceInfo
|
||||
{
|
||||
DeviceId = d.DeviceId,
|
||||
DeviceName = d.DeviceName,
|
||||
LinkedAt = d.LinkedAt,
|
||||
LastUsedAt = d.LastUsedAt
|
||||
}).ToList(),
|
||||
Gold = account.User?.Gold ?? 0,
|
||||
Cash = account.User?.Cash ?? 0,
|
||||
Level = account.User?.Level ?? 1,
|
||||
CarsOwned = carsOwned
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Account?> ValidateTokenAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? "RR3CommunityServer_DefaultSecret_ChangeThis");
|
||||
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
}, out SecurityToken validatedToken);
|
||||
|
||||
var jwtToken = (JwtSecurityToken)validatedToken;
|
||||
var accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
|
||||
|
||||
return await _context.Set<Account>().FindAsync(accountId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string HashPassword(string password)
|
||||
{
|
||||
// Use BCrypt for password hashing
|
||||
return BCrypt.Net.BCrypt.HashPassword(password);
|
||||
}
|
||||
|
||||
private bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return BCrypt.Net.BCrypt.Verify(password, hash);
|
||||
}
|
||||
|
||||
private string GenerateJwtToken(Account account)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"] ?? "RR3CommunityServer_DefaultSecret_ChangeThis");
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim("id", account.Id.ToString()),
|
||||
new Claim("username", account.Username),
|
||||
new Claim("email", account.Email)
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddDays(30),
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
private string GenerateToken()
|
||||
{
|
||||
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,12 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Secret": "RR3CommunityServer_SecureJwtSecret_ChangeThisInProduction_MinimumLength32Characters",
|
||||
"Issuer": "RR3CommunityServer",
|
||||
"Audience": "RR3Community",
|
||||
"ExpiryDays": 30
|
||||
},
|
||||
"AssetsBasePath": "Assets/downloaded",
|
||||
"CustomAssetsPath": "Assets/custom",
|
||||
"ModsPath": "Assets/mods",
|
||||
|
||||
Reference in New Issue
Block a user