diff options
Diffstat (limited to 'code/api/src/Endpoints/Internal/Account')
8 files changed, 266 insertions, 0 deletions
diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs new file mode 100644 index 0000000..ee136a9 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -0,0 +1,64 @@ +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountRoute.Payload>.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer<SharedResources> _localizer; + private readonly EmailValidationService _emailValidation; + private readonly ILogger<CreateAccountRoute> _logger; + private readonly TenantService _tenantService; + + public CreateAccountRoute(UserService userService, MainAppDatabase database, IStringLocalizer<SharedResources> localizer, EmailValidationService emailValidation, TenantService tenantService, ILogger<CreateAccountRoute> logger) { + _userService = userService; + _database = database; + _localizer = localizer; + _emailValidation = emailValidation; + _tenantService = tenantService; + _logger = logger; + } + + public class Payload + { + public string Username { get; set; } + public string Password { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/_/account/create")] + public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) { + var problem = new KnownProblemModel(); + var username = request.Username.Trim(); + if (username.IsValidEmailAddress() == false) { + problem.AddError("username", _localizer["{username} does not look like a valid email", username]); + } else if (_database.Users.FirstOrDefault(c => c.Username == username) != default) { + problem.AddError("username", _localizer["There is already a user registered with username: {username}", username]); + } + + if (request.Password.Length < 6) { + problem.AddError("password", _localizer["The password requires 6 or more characters."]); + } + + if (problem.Errors.Any()) { + problem.Title = _localizer["Invalid form"]; + problem.Subtitle = _localizer["One or more fields is invalid"]; + return KnownProblem(problem); + } + + var user = new User(username); + var tenant = _tenantService.CreateTenant(user.DisplayName() + "'s tenant", user.Id, user.Username); + if (tenant == default) { + _logger.LogError("Not creating new user because the tenant could not be created"); + return KnownProblem(_localizer["Could not create your account, try again soon."]); + } + + user.HashAndSetPassword(request.Password); + _database.Users.Add(user); + await _database.SaveChangesAsync(cancellationToken); + await _userService.LogInUserAsync(HttpContext, user, false, cancellationToken); + await _emailValidation.SendValidationEmailAsync(user); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs new file mode 100644 index 0000000..e1d13dd --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -0,0 +1,32 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly UserService _userService; + + public CreateInitialAccountRoute(MainAppDatabase database, UserService userService) { + _database = database; + _userService = userService; + } + + /// <summary> + /// Create an initial user account. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/account/create-initial")] + public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { + if (_database.Users.Any()) { + return NotFound(); + } + + var user = new User("admin@ivarlovlie.no"); + user.HashAndSetPassword("ivar123"); + _database.Users.Add(user); + await _database.SaveChangesAsync(cancellationToken); + await _userService.LogInUserAsync(HttpContext, user, cancellationToken: cancellationToken); + return Redirect("/"); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs new file mode 100644 index 0000000..f487f74 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly UserService _userService; + + public DeleteAccountRoute(UserService userService) { + _userService = userService; + } + + [HttpDelete("~/_/account/delete")] + public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { + await _userService.LogOutUser(HttpContext, cancellationToken); + await _userService.MarkUserAsDeleted(LoggedInUser.Id, LoggedInUser.Id); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs new file mode 100644 index 0000000..121b40f --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs @@ -0,0 +1,26 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<LoggedInUserModel> +{ + private readonly MainAppDatabase _database; + + public GetAccountRoute(MainAppDatabase database) { + _database = database; + } + + [HttpGet("~/_/account")] + public override async Task<ActionResult<LoggedInUserModel>> HandleAsync(CancellationToken cancellationToken = default) { + var user = _database.Users + .Select(x => new {x.Username, x.Id}) + .SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user != default) { + return Ok(new LoggedInUserModel { + Id = LoggedInUser.Id, + Username = LoggedInUser.Username + }); + } + + await HttpContext.SignOutAsync(); + return Unauthorized(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs new file mode 100644 index 0000000..703f324 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs @@ -0,0 +1,37 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class LoginRoute : RouteBaseAsync.WithRequest<LoginRoute.Payload>.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer<SharedResources> _localizer; + + public LoginRoute(MainAppDatabase database, UserService userService, IStringLocalizer<SharedResources> localizer) { + _database = database; + _userService = userService; + _localizer = localizer; + } + + public class Payload + { + public string Username { get; set; } + public string Password { get; set; } + public bool Persist { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/_/account/login")] + public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) { + var user = _database.Users.FirstOrDefault(u => u.Username == request.Username); + if (user == default || !user.VerifyPassword(request.Password)) { + return KnownProblem(_localizer["Invalid username or password"]); + } + + if (user.Deleted) { + return KnownProblem(_localizer["This user is deleted, please contact support@greatoffice.life if you think this is an error"]); + } + + await _userService.LogInUserAsync(HttpContext, user, request.Persist, cancellationToken); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs new file mode 100644 index 0000000..295d9f6 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class LogoutRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly UserService _userService; + + public LogoutRoute(UserService userService) { + _userService = userService; + } + + /// <summary> + /// Logout a user. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/account/logout")] + public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { + await _userService.LogOutUser(HttpContext, cancellationToken); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs new file mode 100644 index 0000000..1081240 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -0,0 +1,59 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdateAccountRoute.Payload>.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly IStringLocalizer<SharedResources> _localizer; + + public UpdateAccountRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) { + _database = database; + _localizer = localizer; + } + + public class Payload + { + public string Username { get; set; } + + public string Password { get; set; } + } + + [HttpPost("~/_/account/update")] + public override async Task<ActionResult> HandleAsync(Payload request, CancellationToken cancellationToken = default) { + var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await HttpContext.SignOutAsync(); + return Unauthorized(); + } + + if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) { + return KnownProblem(_localizer["Invalid request"], _localizer["No data was submitted"]); + } + + var problem = new KnownProblemModel(); + + if (request.Password.HasValue() && request.Password.Length < 6) { + problem.AddError("password", _localizer["The new password must contain at least 6 characters"]); + } + + if (request.Password.HasValue()) { + user.HashAndSetPassword(request.Password); + } + + if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { + problem.AddError("username", _localizer["The new username does not look like a valid email address"]); + } + + if (problem.Errors.Any()) { + problem.Title = _localizer["Invalid form"]; + problem.Subtitle = _localizer["One or more validation errors occured"]; + return KnownProblem(problem); + } + + if (request.Username.HasValue()) { + user.Username = request.Username.Trim(); + } + + await _database.SaveChangesAsync(cancellationToken); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/_calls.http b/code/api/src/Endpoints/Internal/Account/_calls.http new file mode 100644 index 0000000..78380f5 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Account/_calls.http @@ -0,0 +1,9 @@ +### Login +POST http://localhost:5000/_/account/login +Content-Type: application/json + +{ + "username": "i@oiee.no", + "password": "ivar123", + "persist": false +} |
