From a640703f2da8815dc26ad1600a6f206be1624379 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Wed, 1 Jun 2022 22:10:32 +0200 Subject: feat: Initial after clean slate --- .../Internal/Account/CreateAccountPayload.cs | 17 +++++ .../Internal/Account/CreateAccountRoute.cs | 44 +++++++++++++ .../Internal/Account/CreateGithubSessionRoute.cs | 17 +++++ .../Internal/Account/CreateInitialAccountRoute.cs | 34 ++++++++++ .../Internal/Account/DeleteAccountRoute.cs | 49 +++++++++++++++ .../Endpoints/Internal/Account/GetArchiveRoute.cs | 62 ++++++++++++++++++ server/src/Endpoints/Internal/Account/GetRoute.cs | 30 +++++++++ .../src/Endpoints/Internal/Account/LoginPayload.cs | 22 +++++++ .../src/Endpoints/Internal/Account/LoginRoute.cs | 37 +++++++++++ .../src/Endpoints/Internal/Account/LogoutRoute.cs | 22 +++++++ .../Internal/Account/UpdateAccountPayload.cs | 17 +++++ .../Internal/Account/UpdateAccountRoute.cs | 51 +++++++++++++++ server/src/Endpoints/Internal/BaseRoute.cs | 16 +++++ .../CreateResetRequestRoute.cs | 59 +++++++++++++++++ .../FulfillResetRequestPayload.cs | 14 +++++ .../FulfillResetRequestRoute.cs | 34 ++++++++++ .../IsResetRequestValidRoute.cs | 29 +++++++++ .../Internal/Root/GetApplicationVersionRoute.cs | 21 +++++++ server/src/Endpoints/Internal/Root/LogRoute.cs | 16 +++++ server/src/Endpoints/Internal/RouteBaseAsync.cs | 73 ++++++++++++++++++++++ server/src/Endpoints/Internal/RouteBaseSync.cs | 53 ++++++++++++++++ 21 files changed, 717 insertions(+) create mode 100644 server/src/Endpoints/Internal/Account/CreateAccountPayload.cs create mode 100644 server/src/Endpoints/Internal/Account/CreateAccountRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/GetArchiveRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/GetRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/LoginPayload.cs create mode 100644 server/src/Endpoints/Internal/Account/LoginRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/LogoutRoute.cs create mode 100644 server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs create mode 100644 server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs create mode 100644 server/src/Endpoints/Internal/BaseRoute.cs create mode 100644 server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs create mode 100644 server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs create mode 100644 server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs create mode 100644 server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs create mode 100644 server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs create mode 100644 server/src/Endpoints/Internal/Root/LogRoute.cs create mode 100644 server/src/Endpoints/Internal/RouteBaseAsync.cs create mode 100644 server/src/Endpoints/Internal/RouteBaseSync.cs (limited to 'server/src/Endpoints/Internal') diff --git a/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs b/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs new file mode 100644 index 0000000..dc73e68 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for creating new user accounts. +/// +public class CreateAccountPayload +{ + /// + /// Username for the new account. + /// + public string Username { get; set; } + + /// + /// Password for the new account. + /// + public string Password { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs new file mode 100644 index 0000000..954fbf5 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -0,0 +1,44 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +public class CreateAccountRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public CreateAccountRoute(UserService userService, AppDbContext context) { + _userService = userService; + _context = context; + } + + /// + /// Create a new user account. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/account/create")] + public override async Task HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) { + if (request.Username.IsValidEmailAddress() == false) { + return BadRequest(new ErrorResult("Invalid form", request.Username + " does not look like a valid email")); + } + + if (request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid form", "The password requires 6 or more characters.")); + } + + var username = request.Username.Trim(); + if (_context.Users.Any(c => c.Username == username)) { + return BadRequest(new ErrorResult("Username is not available", "There is already a user registered with email: " + username)); + } + + var user = new User(username); + user.HashAndSetPassword(request.Password); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs b/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs new file mode 100644 index 0000000..0cd1aa5 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateGithubSessionRoute.cs @@ -0,0 +1,17 @@ +using AspNet.Security.OAuth.GitHub; + +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class CreateGithubSessionRoute : RouteBaseSync.WithRequest.WithActionResult +{ + public CreateGithubSessionRoute(IConfiguration configuration) { } + + [AllowAnonymous] + [HttpGet("~/_/account/create-github-session")] + public override ActionResult Handle(string next) { + return Challenge(new AuthenticationProperties { + RedirectUri = next + }, + GitHubAuthenticationDefaults.AuthenticationScheme); + } +} diff --git a/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs b/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs new file mode 100644 index 0000000..13fbdf4 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -0,0 +1,34 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public CreateInitialAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Create an initial user account. + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/account/create-initial")] + public override async Task HandleAsync(CancellationToken cancellationToken = default) { + if (_context.Users.Any()) { + return NotFound(); + } + + var user = new User("admin@ivarlovlie.no"); + user.HashAndSetPassword("ivar123"); + _context.Users.Add(user); + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + return Redirect("/"); + } +} diff --git a/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs b/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs new file mode 100644 index 0000000..2149e15 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/DeleteAccountRoute.cs @@ -0,0 +1,49 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + private readonly UserService _userService; + + /// + public DeleteAccountRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Delete the logged on user's account. + /// + /// + /// + [HttpDelete("~/_/account/delete")] + public override async Task HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await _userService.LogOutUser(HttpContext); + return Unauthorized(); + } + + if (user.Username == "demo@demo.demo") { + await _userService.LogOutUser(HttpContext); + return Ok(); + } + + var githubMappings = _context.TimeCategories.Where(c => c.UserId == user.Id); + var passwordResets = _context.ForgotPasswordRequests.Where(c => c.UserId == user.Id); + var entries = _context.TimeEntries.Where(c => c.UserId == user.Id); + var labels = _context.TimeLabels.Where(c => c.UserId == user.Id); + var categories = _context.TimeCategories.Where(c => c.UserId == user.Id); + + _context.TimeCategories.RemoveRange(githubMappings); + _context.ForgotPasswordRequests.RemoveRange(passwordResets); + _context.TimeEntries.RemoveRange(entries); + _context.TimeLabels.RemoveRange(labels); + _context.TimeCategories.RemoveRange(categories); + _context.Users.Remove(user); + + await _context.SaveChangesAsync(cancellationToken); + await _userService.LogOutUser(HttpContext); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs b/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs new file mode 100644 index 0000000..44f5249 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/GetArchiveRoute.cs @@ -0,0 +1,62 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountArchiveRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public GetAccountArchiveRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get a data archive with the currently logged on user's data. + /// + /// + /// + [HttpGet("~/_/account/archive")] + public override async Task> HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await HttpContext.SignOutAsync(); + return Unauthorized(); + } + + var entries = _context.TimeEntries + .AsNoTracking() + .Include(c => c.Labels) + .Include(c => c.Category) + .Where(c => c.User.Id == user.Id) + .ToList(); + + var jsonOptions = new JsonSerializerOptions { + WriteIndented = true + }; + + var dto = new UserArchiveDto(user); + dto.Entries.AddRange(entries.Select(entry => new UserArchiveDto.EntryDto { + CreatedAt = entry.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"), + StartDateTime = entry.Start, + StopDateTime = entry.Stop, + Description = entry.Description, + Labels = entry.Labels + .Select(c => new UserArchiveDto.LabelDto { + Name = c.Name, + Color = c.Color + }) + .ToList(), + Category = new UserArchiveDto.CategoryDto { + Name = entry.Category.Name, + Color = entry.Category.Color + }, + })); + + dto.CountEntries(); + + var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions); + + return File(entriesSerialized, + "application/json", + user.Username + "-time-tracker-archive-" + DateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json"); + } +} diff --git a/server/src/Endpoints/Internal/Account/GetRoute.cs b/server/src/Endpoints/Internal/Account/GetRoute.cs new file mode 100644 index 0000000..34a3c97 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/GetRoute.cs @@ -0,0 +1,30 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public GetAccountRoute(AppDbContext context) { + _context = context; + } + + /// + /// Get the logged on user's session data. + /// + /// + /// + [HttpGet("~/_/account")] + public override async Task> HandleAsync(CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user != default) { + return Ok(new LoggedInUserModel { + Id = LoggedInUser.Id, + Username = LoggedInUser.Username + }); + } + + await HttpContext.SignOutAsync(); + return Unauthorized(); + } +} diff --git a/server/src/Endpoints/Internal/Account/LoginPayload.cs b/server/src/Endpoints/Internal/Account/LoginPayload.cs new file mode 100644 index 0000000..807662c --- /dev/null +++ b/server/src/Endpoints/Internal/Account/LoginPayload.cs @@ -0,0 +1,22 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for logging in a user. +/// +public class LoginPayload +{ + /// + /// Username of the user's account. + /// + public string Username { get; set; } + + /// + /// Password of the user's account. + /// + public string Password { get; set; } + + /// + /// Specify that the created session should be long lived and continually refreshed. + /// + public bool Persist { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/LoginRoute.cs b/server/src/Endpoints/Internal/Account/LoginRoute.cs new file mode 100644 index 0000000..5b41c61 --- /dev/null +++ b/server/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 AppDbContext _context; + private readonly UserService _userService; + + /// + public LoginRoute(AppDbContext context, UserService userService) { + _context = context; + _userService = userService; + } + + /// + /// Login a user. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/account/login")] + public override async Task HandleAsync(LoginPayload request, CancellationToken cancellationToken = default) { + if (!ModelState.IsValid) { + return BadRequest(ModelState); + } + + var user = _context.Users.SingleOrDefault(u => u.Username == request.Username); + if (user == default || !user.VerifyPassword(request.Password)) { + return BadRequest(new ErrorResult("Invalid username or password")); + } + + await _userService.LogInUser(HttpContext, user, request.Persist); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/LogoutRoute.cs b/server/src/Endpoints/Internal/Account/LogoutRoute.cs new file mode 100644 index 0000000..4a06f4a --- /dev/null +++ b/server/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); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs b/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs new file mode 100644 index 0000000..88a3237 --- /dev/null +++ b/server/src/Endpoints/Internal/Account/UpdateAccountPayload.cs @@ -0,0 +1,17 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +/// +/// Payload for updating an account. +/// +public class UpdatePayload +{ + /// + /// Username to set on the logged on user's account. + /// + public string Username { get; set; } + + /// + /// Password to set on the logged on user's account. + /// + public string Password { get; set; } +} diff --git a/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs b/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs new file mode 100644 index 0000000..a997dcb --- /dev/null +++ b/server/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -0,0 +1,51 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; + +public class UpdateAccountRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly AppDbContext _context; + + /// + public UpdateAccountRoute(AppDbContext context) { + _context = context; + } + + /// + /// Update the logged on user's data. + /// + /// + /// + /// + [HttpPost("~/_/account/update")] + public override async Task HandleAsync(UpdatePayload request, CancellationToken cancellationToken = default) { + var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { + await HttpContext.SignOutAsync(); + return Unauthorized(); + } + + if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) { + return BadRequest(new ErrorResult("Invalid request", "No data was submitted")); + } + + if (request.Password.HasValue() && request.Password.Length < 6) { + return BadRequest(new ErrorResult("Invalid request", + "The new password must contain at least 6 characters")); + } + + if (request.Password.HasValue()) { + user.HashAndSetPassword(request.Password); + } + + if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { + return BadRequest(new ErrorResult("Invalid request", + "The new username does not look like a valid email address")); + } + + if (request.Username.HasValue()) { + user.Username = request.Username.Trim(); + } + + await _context.SaveChangesAsync(cancellationToken); + return Ok(); + } +} diff --git a/server/src/Endpoints/Internal/BaseRoute.cs b/server/src/Endpoints/Internal/BaseRoute.cs new file mode 100644 index 0000000..3e2c6af --- /dev/null +++ b/server/src/Endpoints/Internal/BaseRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal; + +[Authorize] +[ApiController] +[ApiExplorerSettings(IgnoreApi = true)] +[ApiVersionNeutral] +public class BaseRoute : ControllerBase +{ + /// + /// User data for the currently logged on user. + /// + protected LoggedInUserModel LoggedInUser => new() { + Username = User.FindFirstValue(AppClaims.NAME), + Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), + }; +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs new file mode 100644 index 0000000..3e086f6 --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -0,0 +1,59 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class CreateResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly ILogger _logger; + private readonly ForgotPasswordService _forgotPasswordService; + private readonly AppDbContext _context; + + /// + public CreateResetRequestRoute(ILogger logger, ForgotPasswordService forgotPasswordService, AppDbContext context) { + _logger = logger; + _forgotPasswordService = forgotPasswordService; + _context = context; + } + + /// + /// Create a new password reset request. + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/create")] + public override async Task HandleAsync(string username, CancellationToken cancellationToken = default) { + if (!username.IsValidEmailAddress()) { + _logger.LogInformation("Username is invalid, not doing request for password change"); + return BadRequest(new ErrorResult("Invalid email address", username + " looks like an invalid email address")); + } + + Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader); + var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC"); + var offset = tz.BaseUtcOffset.Hours; + + // this is fine as long as the client is not connecting from Australia: Lord Howe Island + // according to https://en.wikipedia.org/wiki/Daylight_saving_time_by_country + if (tz.IsDaylightSavingTime(DateTime.UtcNow)) { + offset++; + } + + _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz); + _logger.LogInformation("Creating forgot password request with date time: " + requestDateTime.ToString("u")); + + try { + var user = _context.Users.SingleOrDefault(c => c.Username.Equals(username)); + if (user != default) { + await _forgotPasswordService.AddRequestAsync(user, tz, cancellationToken); + return Ok(); + } + + _logger.LogInformation("User was not found, not doing request for password change"); + return Ok(); + } catch (Exception e) { + _logger.LogError(e, "ForgotAction failed badly"); + return Ok(); + } + } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs new file mode 100644 index 0000000..f0fb59f --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestPayload.cs @@ -0,0 +1,14 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +public class FulfillResetRequestPayload +{ + /// + /// Id of the password reset request to fulfill + /// + public Guid Id { get; set; } + + /// + /// New password to set on the relevant account + /// + public string NewPassword { get; set; } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs new file mode 100644 index 0000000..e33a4fb --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -0,0 +1,34 @@ + +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// + public FulfillResetRequestRoute(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// + /// Fulfill a password reset request. + /// + /// + /// + /// + [AllowAnonymous] + [HttpPost("~/_/forgot-password-requests/fulfill")] + public override async Task HandleAsync(FulfillResetRequestPayload request, CancellationToken cancellationToken = default) { + try { + var fulfilled = await _forgotPasswordService.FullFillRequestAsync(request.Id, request.NewPassword, cancellationToken); + return Ok(fulfilled); + } catch (Exception e) { + if (e is ForgotPasswordRequestNotFoundException or UserNotFoundException) { + return NotFound(); + } + + throw; + } + } +} diff --git a/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs b/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs new file mode 100644 index 0000000..9984094 --- /dev/null +++ b/server/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs @@ -0,0 +1,29 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; + +/// +public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest.WithActionResult +{ + private readonly ForgotPasswordService _forgotPasswordService; + + /// + public IsResetRequestValidRoute(ForgotPasswordService forgotPasswordService) { + _forgotPasswordService = forgotPasswordService; + } + + /// + /// Check if a given password reset request is still valid. + /// + /// + /// + /// + [AllowAnonymous] + [HttpGet("~/_/forgot-password-requests/is-valid")] + public override async Task HandleAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _forgotPasswordService.GetRequestAsync(id, cancellationToken); + if (request == default) { + return NotFound(); + } + + return Ok(request.IsExpired == false); + } +} diff --git a/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs b/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs new file mode 100644 index 0000000..5fb8213 --- /dev/null +++ b/server/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs @@ -0,0 +1,21 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class GetApplicationVersionRoute : RouteBaseSync.WithoutRequest.WithActionResult +{ + private readonly IWebHostEnvironment _environment; + + /// + public GetApplicationVersionRoute(IWebHostEnvironment environment) { + _environment = environment; + } + + /// + /// Get the running api version number. + /// + /// + [HttpGet("~/_/version")] + public override ActionResult Handle() { + var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt"); + return Ok(System.IO.File.ReadAllText(versionFilePath)); + } +} diff --git a/server/src/Endpoints/Internal/Root/LogRoute.cs b/server/src/Endpoints/Internal/Root/LogRoute.cs new file mode 100644 index 0000000..48b497a --- /dev/null +++ b/server/src/Endpoints/Internal/Root/LogRoute.cs @@ -0,0 +1,16 @@ +namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; + +public class LogRoute : RouteBaseSync.WithRequest.WithoutResult +{ + private readonly ILogger _logger; + + public LogRoute(ILogger logger) { + _logger = logger; + } + + [AllowAnonymous] + [HttpPost("~/_/log")] + public override void Handle([FromBody] string request) { + _logger.LogInformation(request); + } +} diff --git a/server/src/Endpoints/Internal/RouteBaseAsync.cs b/server/src/Endpoints/Internal/RouteBaseAsync.cs new file mode 100644 index 0000000..1bb0af0 --- /dev/null +++ b/server/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 : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + } +} diff --git a/server/src/Endpoints/Internal/RouteBaseSync.cs b/server/src/Endpoints/Internal/RouteBaseSync.cs new file mode 100644 index 0000000..173999d --- /dev/null +++ b/server/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 : BaseRoute + { + public abstract TResponse Handle(TRequest request); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(TRequest request); + } + } + + public static class WithoutRequest + { + public abstract class WithResult : BaseRoute + { + public abstract TResponse Handle(); + } + + public abstract class WithoutResult : BaseRoute + { + public abstract void Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + + public abstract class WithActionResult : BaseRoute + { + public abstract ActionResult Handle(); + } + } +} -- cgit v1.3