Move registration to AuthenticationService, add scripts for UserLogin and UserRegistration

This commit is contained in:
Pat Hartl 2025-02-04 18:45:24 -06:00
parent 117f2d6c2b
commit e9e5bc096d
6 changed files with 197 additions and 74 deletions

View file

@ -3,16 +3,10 @@ using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using LANCommander.SDK.PowerShell;
using Microsoft.Extensions.Logging;
using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.Serialization;
namespace LANCommander.SDK.Services
{
@ -41,6 +35,78 @@ namespace LANCommander.SDK.Services
Client = client;
Logger = logger;
}
#region Authentication Scripts
public async Task RunUserLoginScript(Script loginScript, User user)
{
try
{
using (var op = Logger.BeginOperation("Executing user login script"))
{
var script = new PowerShellScript(Enums.ScriptType.UserLogin);
script.AddVariable("User", user);
script.AddVariable("ServerAddress", Client.BaseUrl.ToString());
script.UseInline(loginScript.Contents);
try
{
op
.Enrich("UserId", user.Id)
.Enrich("Username", user.UserName)
.Enrich("ScriptId", loginScript.Id)
.Enrich("ScriptName", loginScript.Name);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not enrich logs");
}
await script.ExecuteAsync<int>();
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not execute user login script");
}
}
public async Task RunUserRegistrationScript(Script registrationScript, User user)
{
try
{
using (var op = Logger.BeginOperation("Executing user registration script"))
{
var script = new PowerShellScript(Enums.ScriptType.UserRegistration);
script.AddVariable("User", user);
script.AddVariable("ServerAddress", Client.BaseUrl.ToString());
script.UseInline(registrationScript.Contents);
try
{
op
.Enrich("UserId", user.Id)
.Enrich("Username", user.UserName)
.Enrich("ScriptId", registrationScript.Id)
.Enrich("ScriptName", registrationScript.Name);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not enrich logs");
}
await script.ExecuteAsync<int>();
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not execute user registration script");
}
}
#endregion
#region Redistributables
public async Task<bool> RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid redistributableId)

View file

@ -3,20 +3,52 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using AutoMapper;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Models;
using LANCommander.Server.Services.Exceptions;
using Microsoft.IdentityModel.Tokens;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
using PascalCaseNamingConvention = YamlDotNet.Serialization.NamingConventions.PascalCaseNamingConvention;
namespace LANCommander.Server.Services
{
public class AuthenticationService(ILogger<AuthenticationService> logger, UserService userService) : BaseService(logger)
public class AuthenticationService(
ILogger<AuthenticationService> logger,
IMapper mapper,
UserService userService,
RoleService roleService,
ScriptService scriptService) : BaseService(logger)
{
public async Task<AuthToken> LoginAsync(string userName, string password)
{
if (!String.IsNullOrWhiteSpace(userName) && await userService.CheckPassword(userName, password))
{
return await LoginAsync(userName);
var token = await LoginAsync(userName);
try
{
var user = await userService.GetAsync<User>(userName);
var scripts = await scriptService.GetAsync<SDK.Models.Script>(s => s.Type == ScriptType.UserLogin);
if (scripts.Any())
{
var client = new SDK.Client(_settings.Beacon.Address, "", logger);
client.UseToken(token);
foreach (var script in scripts)
{
await client.Scripts.RunUserLoginScript(script, user);
}
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Could not execute user login script");
}
return token;
}
else
throw new Exception("Invalid username or password");
@ -108,6 +140,76 @@ namespace LANCommander.Server.Services
Expiration = newAccessToken.ValidTo
};
}
public async Task<AuthToken> RegisterAsync(string userName, string password, string passwordConfirmation)
{
if (password != passwordConfirmation)
throw new UserRegistrationException("Passwords don't match");
var user = await userService.GetAsync(userName);
if (user != null)
{
_logger?.LogDebug("Cannot register user with username {UserName}, already exists", userName);
throw new UserRegistrationException("Username is unavailable");
}
user = new Data.Models.User();
user.UserName = userName;
user = await userService.AddAsync(user);
if (user != null)
{
await userService.ChangePassword(user.UserName, password);
try
{
if (_settings.Roles.DefaultRoleId == Guid.Empty)
{
var defaultRole = await roleService.GetAsync(_settings.Roles.DefaultRoleId);
if (defaultRole != null)
await userService.AddToRoleAsync(user.UserName, defaultRole.Name);
}
var token = await LoginAsync(user.UserName, password);
logger?.LogDebug("Successfully registered user {UserName}", user.UserName);
try
{
var scripts = await scriptService.GetAsync<SDK.Models.Script>(s => s.Type == ScriptType.UserLogin);
if (scripts.Any())
{
var client = new SDK.Client(_settings.Beacon.Address, "", logger);
client.UseToken(token);
foreach (var script in scripts)
{
await client.Scripts.RunUserRegistrationScript(script, mapper.Map<SDK.Models.User>(user));
}
}
}
catch (Exception ex)
{
logger?.LogError(ex, "Could not execute user login script");
}
return token;
}
catch (Exception ex)
{
logger?.LogError(ex, "Could not register user {UserName}", user.UserName);
throw new UserRegistrationException("An unknown error occurred while registering");
}
}
else
throw new UserRegistrationException("Unknown Error");
}
private ClaimsPrincipal? GetPrincipalFromExpiredToken(string? token)
{

View file

@ -6,6 +6,10 @@ public class UserRegistrationException : Exception
{
public IdentityResult IdentityResult { get; set; }
public UserRegistrationException(string message) : base(message)
{
}
public UserRegistrationException(IdentityResult identityResult, string message) : base(message)
{
IdentityResult = identityResult;

View file

@ -17,13 +17,6 @@ using User = LANCommander.Server.Data.Models.User;
namespace LANCommander.Server.Controllers.Api
{
public class TokenModel
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime Expiration { get; set; }
}
public class LoginModel
{
public string UserName { get; set; }
@ -147,64 +140,17 @@ namespace LANCommander.Server.Controllers.Api
[HttpPost("Register")]
public async Task<IActionResult> RegisterAsync([FromBody] RegisterModel model)
{
if (model.Password != model.PasswordConfirmation)
return Unauthorized(new
{
Message = "Passwords don't match."
});
var user = await UserService.GetAsync(model.UserName);
if (user != null)
try
{
Logger?.LogDebug("Cannot register user with username {UserName}, already exists", model.UserName);
var token = await AuthenticationService.RegisterAsync(model.UserName, model.Password,
model.PasswordConfirmation);
return Unauthorized(new
{
Message = "Username is unavailable"
});
return Ok(token);
}
user = new User();
user.UserName = model.UserName;
user = await UserService.AddAsync(user);
if (user != null)
catch (Exception ex)
{
await UserService.ChangePassword(user.UserName, model.Password);
try
{
if (Settings.Roles.DefaultRoleId != Guid.Empty)
{
var defaultRole = await RoleService.GetAsync(Settings.Roles.DefaultRoleId);
if (defaultRole != null)
await UserService.AddToRoleAsync(user.UserName, defaultRole.Name);
}
var token = await AuthenticationService.LoginAsync(user.UserName, model.Password);
Logger?.LogDebug("Successfully registered user {UserName}", user.UserName);
return Ok(token);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not register user {UserName}", user.UserName);
return BadRequest(new
{
Message = "An unknown error occurred"
});
}
return Unauthorized(ex.Message);
}
return Unauthorized(new
{
//Message = "Error:\n" + String.Join('\n', result.Errors.Select(e => e.Description))
});
}
[HttpGet("AuthenticationProviders")]

View file

@ -9,7 +9,7 @@
TItem="Script"
HidePagination
Responsive
Query="s => (s.GameId != Guid.Empty && s.GameId == GameId) || (s.RedistributableId != Guid.Empty && s.RedistributableId == RedistributableId) || (s.ServerId != Guid.Empty && s.ServerId == ServerId)">
Query="s => s.GameId == Guid.Empty && s.RedistributableId == Guid.Empty && s.ServerId == Guid.Empty">
<BoundDataColumn Property="s => s.Type">
@context.Type.GetDisplayName()
</BoundDataColumn>
@ -63,10 +63,6 @@
{
ScriptId = scriptId ?? default,
AllowedTypes = AllowedTypes,
ArchiveId = ArchiveId,
GameId = GameId,
RedistributableId = RedistributableId,
ServerId = ServerId
};
var modalRef = await ModalService.CreateModalAsync<ScriptEditorDialog, ScriptEditorOptions, Script>(modalOptions, options);

View file

@ -0,0 +1,9 @@
@page "/Settings/Scripts"
@using LANCommander.SDK.Enums
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
<PageHeader Title="Scripts" />
<div style="padding: 0 24px;">
<ScriptEditor AllowedTypes="new [] { ScriptType.UserLogin, ScriptType.UserRegistration }" />
</div>