From 900bb5e845c3ad44defbd427cae3d44a4a43321f Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Sat, 25 Feb 2023 13:15:44 +0100 Subject: feat: Initial commit --- .../Internal/Account/CreateAccountRoute.cs | 64 +++++++++++++++++++ .../Internal/Account/CreateInitialAccountRoute.cs | 32 ++++++++++ .../Internal/Account/DeleteAccountRoute.cs | 17 +++++ .../Endpoints/Internal/Account/GetAccountRoute.cs | 26 ++++++++ .../src/Endpoints/Internal/Account/LoginRoute.cs | 37 +++++++++++ .../src/Endpoints/Internal/Account/LogoutRoute.cs | 22 +++++++ .../Internal/Account/UpdateAccountRoute.cs | 59 +++++++++++++++++ .../api/src/Endpoints/Internal/Account/_calls.http | 9 +++ .../api/src/Endpoints/Internal/INT_EndpointBase.cs | 9 +++ .../CreateResetRequestRoute.cs | 40 ++++++++++++ .../FulfillResetRequestRoute.cs | 36 +++++++++++ .../IsResetRequestValidRoute.cs | 26 ++++++++ .../Internal/PasswordResetRequests/_calls.http | 22 +++++++ .../src/Endpoints/Internal/Root/GetSessionRoute.cs | 64 +++++++++++++++++++ .../Internal/Root/IsAuthenticatedRoute.cs | 10 +++ .../Internal/Root/ReadConfigurationRoute.cs | 17 +++++ .../Internal/Root/RefreshConfigurationRoute.cs | 15 +++++ .../src/Endpoints/Internal/Root/ValidateRoute.cs | 39 ++++++++++++ code/api/src/Endpoints/Internal/RouteBaseAsync.cs | 73 ++++++++++++++++++++++ code/api/src/Endpoints/Internal/RouteBaseSync.cs | 53 ++++++++++++++++ 20 files changed, 670 insertions(+) create mode 100644 code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/GetAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/LoginRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/LogoutRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Account/_calls.http create mode 100644 code/api/src/Endpoints/Internal/INT_EndpointBase.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs create mode 100644 code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http create mode 100644 code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs create mode 100644 code/api/src/Endpoints/Internal/Root/ValidateRoute.cs create mode 100644 code/api/src/Endpoints/Internal/RouteBaseAsync.cs create mode 100644 code/api/src/Endpoints/Internal/RouteBaseSync.cs (limited to 'code/api/src/Endpoints/Internal') 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.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer _localizer; + private readonly EmailValidationService _emailValidation; + private readonly ILogger _logger; + private readonly TenantService _tenantService; + + public CreateAccountRoute(UserService userService, MainAppDatabase database, IStringLocalizer localizer, EmailValidationService emailValidation, TenantService tenantService, ILogger 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 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; + } + + /// + /// Create an initial user account. + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/account/create-initial")] + public override async Task 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 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 +{ + private readonly MainAppDatabase _database; + + public GetAccountRoute(MainAppDatabase database) { + _database = database; + } + + [HttpGet("~/_/account")] + public override async Task> 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.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly UserService _userService; + private readonly IStringLocalizer _localizer; + + public LoginRoute(MainAppDatabase database, UserService userService, IStringLocalizer 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 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; + } + + /// + /// Logout a user. + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/account/logout")] + public override async Task 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.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly IStringLocalizer _localizer; + + public UpdateAccountRoute(MainAppDatabase database, IStringLocalizer 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 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 +} diff --git a/code/api/src/Endpoints/Internal/INT_EndpointBase.cs b/code/api/src/Endpoints/Internal/INT_EndpointBase.cs new file mode 100644 index 0000000..699a976 --- /dev/null +++ b/code/api/src/Endpoints/Internal/INT_EndpointBase.cs @@ -0,0 +1,9 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +[Authorize] +[ApiExplorerSettings(IgnoreApi = true)] +[ApiVersionNeutral] +public class INT_EndpointBase : EndpointBase +{ + +} diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs new file mode 100644 index 0000000..c6ed417 --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -0,0 +1,40 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class CreateResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly ILogger _logger; + private readonly PasswordResetService _passwordResetService; + private readonly MainAppDatabase _database; + private readonly IStringLocalizer _localizer; + + public CreateResetRequestRoute(ILogger logger, PasswordResetService passwordResetService, MainAppDatabase database, IStringLocalizer localizer) { + _logger = logger; + _passwordResetService = passwordResetService; + _database = database; + _localizer = localizer; + } + + public class Payload + { + public string Email { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/_/password-reset-request/create")] + public override async Task HandleAsync(Payload payload, CancellationToken cancellationToken = default) { + if (payload.Email.IsNullOrWhiteSpace()) { + return KnownProblem(_localizer["Invalid form"], + _localizer["One or more fields is invalid"], + new() {{"email", new string[] {_localizer["Email is a required field"]}}} + ); + } + + var tz = GetRequestTimeZone(_logger); + _logger.LogInformation("Creating forgot password request with local date time: " + tz.LocalDateTime.ToString("u")); + var user = _database.Users.FirstOrDefault(c => c.Username.Equals(payload.Email)); + // Don't inform the caller that the user does not exist. + if (user == default) return Ok(); + await _passwordResetService.AddRequestAsync(user, tz.TimeZoneInfo, cancellationToken); + return Ok(); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs new file mode 100644 index 0000000..9cd92bb --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -0,0 +1,36 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly IStringLocalizer _localizer; + private readonly PasswordResetService _passwordResetService; + + public FulfillResetRequestRoute(PasswordResetService passwordResetService, IStringLocalizer localizer) { + _passwordResetService = passwordResetService; + _localizer = localizer; + } + + public class Payload + { + public Guid Id { get; set; } + public string NewPassword { get; set; } + } + + [AllowAnonymous] + [HttpPost("~/_/password-reset-request/fulfill")] + public override async Task HandleAsync(Payload request, CancellationToken cancellationToken = default) { + if (request.NewPassword.Length < 6) { + return KnownProblem(_localizer["Invalid form"], + _localizer["One or more fields is invalid"], + new Dictionary {{"newPassword", new string[] {_localizer["The new password needs to be atleast 6 characters"]}}} + ); + } + + return await _passwordResetService.FulfillRequestAsync(request.Id, request.NewPassword, cancellationToken) switch { + FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND => NotFound(), + FulfillPasswordResetRequestResult.USER_NOT_FOUND => NotFound(), + FulfillPasswordResetRequestResult.FULFILLED => Ok(), + _ => StatusCode(500) + }; + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs new file mode 100644 index 0000000..a87c0a9 --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs @@ -0,0 +1,26 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly PasswordResetService _passwordResetService; + + public IsResetRequestValidRoute(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } + + public class ResponseModel + { + public ResponseModel(bool isValid) { + IsValid = isValid; + } + + public bool IsValid { get; } + } + + [AllowAnonymous] + [HttpGet("~/_/password-reset-request/is-valid")] + public override async Task> HandleAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _passwordResetService.GetRequestAsync(id, cancellationToken); + return Ok(request == default ? new ResponseModel(false) : new ResponseModel(!request.IsExpired)); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http b/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http new file mode 100644 index 0000000..cfd2d58 --- /dev/null +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/_calls.http @@ -0,0 +1,22 @@ +### Create request +POST http://localhost:5000/_/password-reset-request/create +Accept: application/json +Content-Type: application/json + +{ + "email": "" +} + +### Fulfill request +POST http://localhost:5000/_/password-reset-request/fulfill +Accept: application/json +Content-Type: application/json + +{ + "id": "", + "newPassword": "" +} + +### Is request valid +GET http://localhost:5000/_/password-reset-request/is-valid?id= +Accept: application/json diff --git a/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs b/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs new file mode 100644 index 0000000..82bbb11 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/GetSessionRoute.cs @@ -0,0 +1,64 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class GetSessionRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly ILogger _logger; + + public GetSessionRoute(MainAppDatabase database, ILogger logger) { + _database = database; + _logger = logger; + } + + public class SessionResponse + { + public string Username { get; set; } + public string DisplayName { get; set; } + public Guid UserId { get; set; } + public SessionTenant CurrentTenant { get; set; } + public List AvailableTenants { get; set; } + + public class SessionTenant + { + public Guid Id { get; set; } + public string Name { get; set; } + } + } + + [Authorize] + [HttpGet("~/_/session-data")] + public override ActionResult Handle() { + var user = _database.Users.Include(c => c.Tenants) + .Select(c => new User() { + Id = c.Id, + Username = c.Username, + FirstName = c.FirstName, + LastName = c.LastName, + Tenants = c.Tenants + }).FirstOrDefault(c => c.Id == LoggedInUser.Id); + + if (user == default) { + return NotFound(); + } + + var currentTenant = user.Tenants.FirstOrDefault(c => c.Id == LoggedInUser.TenantId); + if (currentTenant == default) { + _logger.LogInformation("Could not find current tenant ({tenantId}) for user {userId}", LoggedInUser.TenantId, LoggedInUser.Id); + return NotFound(); + } + + return Ok(new SessionResponse() { + Username = user.Username, + DisplayName = user.DisplayName(), + UserId = user.Id, + CurrentTenant = new SessionResponse.SessionTenant() { + Id = currentTenant.Id, + Name = currentTenant.Name + }, + AvailableTenants = user.Tenants.Select(c => new SessionResponse.SessionTenant() { + Id = c.Id, + Name = c.Name + }).ToList() + }); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs b/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs new file mode 100644 index 0000000..7bb0a86 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/IsAuthenticatedRoute.cs @@ -0,0 +1,10 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class IsAuthenticatedRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + [Authorize] + [HttpGet("~/_/is-authenticated")] + public override ActionResult Handle() { + return Ok(); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs new file mode 100644 index 0000000..7270fd8 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class ReadConfigurationRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + private readonly VaultService _vaultService; + + public ReadConfigurationRoute(VaultService vaultService) { + _vaultService = vaultService; + } + + [AllowAnonymous] + [HttpGet("~/_/configuration")] + public override ActionResult Handle() { + var config = _vaultService.GetCurrentAppConfiguration(); + return Content(JsonSerializer.Serialize(config.GetPublicVersion()), "application/json"); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs new file mode 100644 index 0000000..fde4832 --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs @@ -0,0 +1,15 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class RefreshConfigurationRoute : RouteBaseSync.WithoutRequest.WithoutResult +{ + private readonly VaultService _vaultService; + + public RefreshConfigurationRoute(VaultService vaultService) { + _vaultService = vaultService; + } + + [HttpGet("~/_/refresh-configuration")] + public override void Handle() { + _vaultService.RefreshCurrentAppConfigurationAsync(); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs new file mode 100644 index 0000000..8f0882d --- /dev/null +++ b/code/api/src/Endpoints/Internal/Root/ValidateRoute.cs @@ -0,0 +1,39 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class ValidateRoute : RouteBaseSync.WithRequest.WithActionResult +{ + private readonly EmailValidationService _emailValidation; + private readonly string CanonicalFrontendUrl; + private readonly ILogger _logger; + + public ValidateRoute(VaultService vaultService, EmailValidationService emailValidation, ILogger logger) { + _emailValidation = emailValidation; + _logger = logger; + var c = vaultService.GetCurrentAppConfiguration(); + CanonicalFrontendUrl = c.CANONICAL_FRONTEND_URL; + } + + public class QueryParams + { + [FromQuery] + public Guid Id { get; set; } + } + + [HttpGet("~/_/validate")] + public override ActionResult Handle([FromQuery] QueryParams request) { + var isFulfilled = _emailValidation.FulfillEmailValidationRequest(request.Id, LoggedInUser.Id); + if (!isFulfilled) { + _logger.LogError("Email validation fulfillment failed for request {requestId} and user {userId}", request.Id, LoggedInUser.Id); + return StatusCode(400, $""" + + +

The validation could not be completed

+

We are working on fixing this, in the meantime, have patience.

+Click here to go back to {CanonicalFrontendUrl} + +"""); + } + + return Redirect(CanonicalFrontendUrl + "/portal?msg=emailValidated"); + } +} \ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/RouteBaseAsync.cs b/code/api/src/Endpoints/Internal/RouteBaseAsync.cs new file mode 100644 index 0000000..a87facf --- /dev/null +++ b/code/api/src/Endpoints/Internal/RouteBaseAsync.cs @@ -0,0 +1,73 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseAsync +{ + public static class WithRequest + { + public abstract class WithResult : INT_EndpointBase + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : INT_EndpointBase + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract Task> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : INT_EndpointBase + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : INT_EndpointBase + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract Task> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + } +} diff --git a/code/api/src/Endpoints/Internal/RouteBaseSync.cs b/code/api/src/Endpoints/Internal/RouteBaseSync.cs new file mode 100644 index 0000000..9d9bd5a --- /dev/null +++ b/code/api/src/Endpoints/Internal/RouteBaseSync.cs @@ -0,0 +1,53 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +/// +/// A base class for an endpoint that accepts parameters. +/// +public static class RouteBaseSync +{ + public static class WithRequest + { + public abstract class WithResult : INT_EndpointBase + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : INT_EndpointBase + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract ActionResult Handle(TRequest request); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : INT_EndpointBase + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : INT_EndpointBase + { + public abstract void Handle(); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract ActionResult Handle(); + } + + public abstract class WithActionResult : INT_EndpointBase + { + public abstract ActionResult Handle(); + } + } +} -- cgit v1.3