Extract game auth service
This commit is contained in:
160
RpgRoller/Services/GameAuthService.cs
Normal file
160
RpgRoller/Services/GameAuthService.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public sealed class GameAuthService
|
||||
{
|
||||
public GameAuthService(GameStateStore stateStore, IPasswordHasher<UserAccount> passwordHasher, GamePersistenceService persistenceService)
|
||||
{
|
||||
m_StateStore = stateStore;
|
||||
m_PasswordHasher = passwordHasher;
|
||||
m_PersistenceService = persistenceService;
|
||||
}
|
||||
|
||||
public ServiceResult<UserSummary> Register(string username, string password, string displayName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return ServiceResult<UserSummary>.Failure("invalid_username", "Username is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return ServiceResult<UserSummary>.Failure("invalid_display_name", "Display name is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length < 8)
|
||||
return ServiceResult<UserSummary>.Failure("invalid_password", "Password must be at least 8 characters.");
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var trimmedUsername = username.Trim();
|
||||
var normalizedUsername = NormalizeUsername(trimmedUsername);
|
||||
if (m_StateStore.UserIdsByUsername.ContainsKey(normalizedUsername))
|
||||
return ServiceResult<UserSummary>.Failure("duplicate_username", "Username is already taken.");
|
||||
|
||||
var user = new UserAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = trimmedUsername,
|
||||
UsernameNormalized = normalizedUsername,
|
||||
DisplayName = displayName.Trim(),
|
||||
PasswordHash = string.Empty,
|
||||
Roles = m_StateStore.UsersById.Count == 0 ? UserRoles.Admin : string.Empty,
|
||||
ActiveCharacterId = null
|
||||
};
|
||||
|
||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||
|
||||
m_StateStore.UsersById[user.Id] = user;
|
||||
m_StateStore.UserIdsByUsername[user.UsernameNormalized] = user.Id;
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<UserSummary>.Success(ToUserSummary(user));
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var normalizedUsername = NormalizeUsername(username.Trim());
|
||||
if (!m_StateStore.UserIdsByUsername.TryGetValue(normalizedUsername, out var userId))
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
var user = m_StateStore.UsersById[userId];
|
||||
var verification = m_PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
if (verification == PasswordVerificationResult.Failed)
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
if (verification == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||
|
||||
var session = CreateSession(userId);
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Success((ToUserSummary(user), session.Token));
|
||||
}
|
||||
}
|
||||
|
||||
public void Logout(string sessionToken)
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
if (m_StateStore.SessionsByToken.Remove(sessionToken))
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public UserSummary? GetUserBySession(string sessionToken)
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
return user is null ? null : ToUserSummary(user);
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<MeResponse> GetMe(string sessionToken)
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<MeResponse>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
Guid? campaignId = null;
|
||||
if (user.ActiveCharacterId is Guid activeCharacterId)
|
||||
{
|
||||
if (!m_StateStore.CharactersById.TryGetValue(activeCharacterId, out var activeCharacter))
|
||||
{
|
||||
user.ActiveCharacterId = null;
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
}
|
||||
else
|
||||
campaignId = activeCharacter.CampaignId;
|
||||
}
|
||||
|
||||
return ServiceResult<MeResponse>.Success(new(ToUserSummary(user), user.ActiveCharacterId, campaignId));
|
||||
}
|
||||
}
|
||||
|
||||
private UserSession CreateSession(Guid userId)
|
||||
{
|
||||
var token = Guid.NewGuid().ToString("N");
|
||||
var session = new UserSession
|
||||
{
|
||||
Token = token,
|
||||
UserId = userId,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
m_StateStore.SessionsByToken[token] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private static UserSummary ToUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
private static string NormalizeUsername(string username)
|
||||
{
|
||||
return username.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private readonly IPasswordHasher<UserAccount> m_PasswordHasher;
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
private readonly GameStateStore m_StateStore;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public sealed class GameService : IGameService
|
||||
m_UserIdsByUsername = m_StateStore.UserIdsByUsername;
|
||||
m_UsersById = m_StateStore.UsersById;
|
||||
m_PersistenceService = new(dbContextFactory, m_StateStore);
|
||||
m_PasswordHasher = passwordHasher;
|
||||
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
||||
m_DiceRoller = diceRoller;
|
||||
LoadStateFromDatabase();
|
||||
}
|
||||
@@ -35,108 +35,27 @@ public sealed class GameService : IGameService
|
||||
|
||||
public ServiceResult<UserSummary> Register(string username, string password, string displayName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return ServiceResult<UserSummary>.Failure("invalid_username", "Username is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return ServiceResult<UserSummary>.Failure("invalid_display_name", "Display name is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length < 8)
|
||||
return ServiceResult<UserSummary>.Failure("invalid_password", "Password must be at least 8 characters.");
|
||||
|
||||
lock (m_Gate)
|
||||
{
|
||||
var trimmedUsername = username.Trim();
|
||||
var normalizedUsername = NormalizeUsername(trimmedUsername);
|
||||
if (m_UserIdsByUsername.ContainsKey(normalizedUsername))
|
||||
return ServiceResult<UserSummary>.Failure("duplicate_username", "Username is already taken.");
|
||||
|
||||
var user = new UserAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = trimmedUsername,
|
||||
UsernameNormalized = normalizedUsername,
|
||||
DisplayName = displayName.Trim(),
|
||||
PasswordHash = string.Empty,
|
||||
Roles = m_UsersById.Count == 0 ? UserRoles.Admin : string.Empty,
|
||||
ActiveCharacterId = null
|
||||
};
|
||||
|
||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||
|
||||
m_UsersById[user.Id] = user;
|
||||
m_UserIdsByUsername[user.UsernameNormalized] = user.Id;
|
||||
|
||||
PersistStateLocked();
|
||||
return ServiceResult<UserSummary>.Success(ToUserSummary(user));
|
||||
}
|
||||
return m_AuthService.Register(username, password, displayName);
|
||||
}
|
||||
|
||||
public ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
lock (m_Gate)
|
||||
{
|
||||
var normalizedUsername = NormalizeUsername(username.Trim());
|
||||
if (!m_UserIdsByUsername.TryGetValue(normalizedUsername, out var userId))
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
var user = m_UsersById[userId];
|
||||
var verification = m_PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
if (verification == PasswordVerificationResult.Failed)
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||
|
||||
if (verification == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||
|
||||
var session = CreateSession(userId);
|
||||
PersistStateLocked();
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Success((ToUserSummary(user), session.Token));
|
||||
}
|
||||
return m_AuthService.Login(username, password);
|
||||
}
|
||||
|
||||
public void Logout(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
if (m_SessionsByToken.Remove(sessionToken))
|
||||
PersistStateLocked();
|
||||
}
|
||||
m_AuthService.Logout(sessionToken);
|
||||
}
|
||||
|
||||
public UserSummary? GetUserBySession(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
return user is null ? null : ToUserSummary(user);
|
||||
}
|
||||
return m_AuthService.GetUserBySession(sessionToken);
|
||||
}
|
||||
|
||||
public ServiceResult<MeResponse> GetMe(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<MeResponse>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
Guid? campaignId = null;
|
||||
if (user.ActiveCharacterId is Guid activeCharacterId)
|
||||
{
|
||||
if (!m_CharactersById.TryGetValue(activeCharacterId, out var activeCharacter))
|
||||
{
|
||||
user.ActiveCharacterId = null;
|
||||
PersistStateLocked();
|
||||
}
|
||||
else
|
||||
campaignId = activeCharacter.CampaignId;
|
||||
}
|
||||
|
||||
return ServiceResult<MeResponse>.Success(new(ToUserSummary(user), user.ActiveCharacterId, campaignId));
|
||||
}
|
||||
return m_AuthService.GetMe(sessionToken);
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||
@@ -1638,20 +1557,6 @@ public sealed class GameService : IGameService
|
||||
return RoleSerializer.HasRole(user.Roles, role);
|
||||
}
|
||||
|
||||
private UserSession CreateSession(Guid userId)
|
||||
{
|
||||
var token = Guid.NewGuid().ToString("N");
|
||||
var session = new UserSession
|
||||
{
|
||||
Token = token,
|
||||
UserId = userId,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
m_SessionsByToken[token] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
@@ -1766,9 +1671,9 @@ public sealed class GameService : IGameService
|
||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById;
|
||||
private readonly Dictionary<Guid, GameCampaignStateTracker> m_CampaignStateById;
|
||||
private readonly Dictionary<Guid, Character> m_CharactersById;
|
||||
private readonly GameAuthService m_AuthService;
|
||||
private readonly IDiceRoller m_DiceRoller;
|
||||
private readonly object m_Gate;
|
||||
private readonly IPasswordHasher<UserAccount> m_PasswordHasher;
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
private readonly List<RollLogEntry> m_RollLog;
|
||||
private readonly Dictionary<string, UserSession> m_SessionsByToken;
|
||||
|
||||
Reference in New Issue
Block a user