diff options
Diffstat (limited to 'code/api/src/Endpoints')
39 files changed, 1154 insertions, 969 deletions
diff --git a/code/api/src/Endpoints/EndpointBase.cs b/code/api/src/Endpoints/EndpointBase.cs index de5d967..a5b0931 100644 --- a/code/api/src/Endpoints/EndpointBase.cs +++ b/code/api/src/Endpoints/EndpointBase.cs @@ -1,3 +1,5 @@ +using ILogger = Microsoft.Extensions.Logging.ILogger; + namespace IOL.GreatOffice.Api.Endpoints; [ApiController] @@ -20,4 +22,32 @@ public class EndpointBase : ControllerBase TraceId = HttpContext.TraceIdentifier }); } + + [NonAction] + protected RequestTimeZoneInfo GetRequestTimeZone(ILogger logger = default) { + 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(AppDateTime.UtcNow)) { + offset++; + } + + logger?.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours"); + + return new RequestTimeZoneInfo() { + TimeZoneInfo = tz, + Offset = offset, + LocalDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz) + }; + } + + public class RequestTimeZoneInfo + { + public TimeZoneInfo TimeZoneInfo { get; set; } + public int Offset { get; set; } + public DateTime LocalDateTime { get; set; } + } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs index dc73e68..1161af3 100644 --- a/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountPayload.cs @@ -5,13 +5,13 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; /// </summary> public class CreateAccountPayload { - /// <summary> - /// Username for the new account. - /// </summary> - public string Username { get; set; } + /// <summary> + /// Username for the new account. + /// </summary> + public string Username { get; set; } - /// <summary> - /// Password for the new account. - /// </summary> - public string Password { get; set; } -} + /// <summary> + /// Password for the new account. + /// </summary> + public string Password { get; set; } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs index 0f4a383..f34056d 100644 --- a/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/CreateAccountRoute.cs @@ -1,44 +1,42 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; -/// <inheritdoc /> public class CreateAccountRoute : RouteBaseAsync.WithRequest<CreateAccountPayload>.WithActionResult { - private readonly AppDbContext _context; - private readonly UserService _userService; + private readonly MainAppDatabase _database; + private readonly UserService _userService; - /// <inheritdoc /> - public CreateAccountRoute(UserService userService, AppDbContext context) { - _userService = userService; - _context = context; - } + public CreateAccountRoute(UserService userService, MainAppDatabase database) { + _userService = userService; + _database = database; + } - /// <summary> - /// Create a new user account. - /// </summary> - /// <param name="request"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [AllowAnonymous] - [HttpPost("~/_/account/create")] - public override async Task<ActionResult> HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) { - if (request.Username.IsValidEmailAddress() == false) { - return BadRequest(new KnownProblemModel("Invalid form", request.Username + " does not look like a valid email")); - } + /// <summary> + /// Create a new user account. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpPost("~/_/account/create")] + public override async Task<ActionResult> HandleAsync(CreateAccountPayload request, CancellationToken cancellationToken = default) { + if (request.Username.IsValidEmailAddress() == false) { + return BadRequest(new KnownProblemModel("Invalid form", request.Username + " does not look like a valid email")); + } - if (request.Password.Length < 6) { - return BadRequest(new KnownProblemModel("Invalid form", "The password requires 6 or more characters.")); - } + if (request.Password.Length < 6) { + return BadRequest(new KnownProblemModel("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 KnownProblemModel("Username is not available", "There is already a user registered with email: " + username)); - } + var username = request.Username.Trim(); + if (_database.Users.Any(c => c.Username == username)) { + return BadRequest(new KnownProblemModel("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(); - } -} + var user = new User(username); + user.HashAndSetPassword(request.Password); + _database.Users.Add(user); + await _database.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, 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 index 13fbdf4..56ff9c6 100644 --- a/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/CreateInitialAccountRoute.cs @@ -1,34 +1,32 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; -/// <inheritdoc /> public class CreateInitialAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult { - private readonly AppDbContext _context; - private readonly UserService _userService; + private readonly MainAppDatabase _database; + private readonly UserService _userService; - /// <inheritdoc /> - public CreateInitialAccountRoute(AppDbContext context, UserService userService) { - _context = context; - _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 (_context.Users.Any()) { - return NotFound(); - } + /// <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"); - _context.Users.Add(user); - await _context.SaveChangesAsync(cancellationToken); - await _userService.LogInUser(HttpContext, user); - return Redirect("/"); - } -} + var user = new User("admin@ivarlovlie.no"); + user.HashAndSetPassword("ivar123"); + _database.Users.Add(user); + await _database.SaveChangesAsync(cancellationToken); + await _userService.LogInUser(HttpContext, user); + 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 index 2149e15..5df1fb6 100644 --- a/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/DeleteAccountRoute.cs @@ -2,48 +2,47 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class DeleteAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult { - private readonly AppDbContext _context; - private readonly UserService _userService; + private readonly MainAppDatabase _database; + private readonly UserService _userService; - /// <inheritdoc /> - public DeleteAccountRoute(AppDbContext context, UserService userService) { - _context = context; - _userService = userService; - } + public DeleteAccountRoute(MainAppDatabase database, UserService userService) { + _database = database; + _userService = userService; + } - /// <summary> - /// Delete the logged on user's account. - /// </summary> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [HttpDelete("~/_/account/delete")] - public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { - var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); - if (user == default) { - await _userService.LogOutUser(HttpContext); - return Unauthorized(); - } + /// <summary> + /// Delete the logged on user's account. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [HttpDelete("~/_/account/delete")] + public override async Task<ActionResult> HandleAsync(CancellationToken cancellationToken = default) { + var user = _database.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(); - } + 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); + var githubMappings = _database.TimeCategories.Where(c => c.UserId == user.Id); + var passwordResets = _database.ForgotPasswordRequests.Where(c => c.UserId == user.Id); + var entries = _database.TimeEntries.Where(c => c.UserId == user.Id); + var labels = _database.TimeLabels.Where(c => c.UserId == user.Id); + var categories = _database.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); + _database.TimeCategories.RemoveRange(githubMappings); + _database.ForgotPasswordRequests.RemoveRange(passwordResets); + _database.TimeEntries.RemoveRange(entries); + _database.TimeLabels.RemoveRange(labels); + _database.TimeCategories.RemoveRange(categories); + _database.Users.Remove(user); - await _context.SaveChangesAsync(cancellationToken); - await _userService.LogOutUser(HttpContext); - return Ok(); - } -} + await _database.SaveChangesAsync(cancellationToken); + await _userService.LogOutUser(HttpContext); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs index f1b70f3..0d9f817 100644 --- a/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/GetArchiveRoute.cs @@ -2,61 +2,60 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class GetAccountArchiveRoute : RouteBaseAsync.WithoutRequest.WithActionResult<UserArchiveDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public GetAccountArchiveRoute(AppDbContext context) { - _context = context; - } + public GetAccountArchiveRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Get a data archive with the currently logged on user's data. - /// </summary> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [HttpGet("~/_/account/archive")] - public override async Task<ActionResult<UserArchiveDto>> HandleAsync(CancellationToken cancellationToken = default) { - var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); - if (user == default) { - await HttpContext.SignOutAsync(); - return Unauthorized(); - } + /// <summary> + /// Get a data archive with the currently logged on user's data. + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [HttpGet("~/_/account/archive")] + public override async Task<ActionResult<UserArchiveDto>> HandleAsync(CancellationToken cancellationToken = default) { + var user = _database.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.UserId == user.Id) - .ToList(); + var entries = _database.TimeEntries + .AsNoTracking() + .Include(c => c.Labels) + .Include(c => c.Category) + .Where(c => c.UserId == user.Id) + .ToList(); - var jsonOptions = new JsonSerializerOptions { - WriteIndented = true - }; + 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 - }, - })); + 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(); + dto.CountEntries(); - var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions); + var entriesSerialized = JsonSerializer.SerializeToUtf8Bytes(dto, jsonOptions); - return File(entriesSerialized, - "application/json", - user.Username + "-time-tracker-archive-" + AppDateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json"); - } -} + return File(entriesSerialized, + "application/json", + user.Username + "-time-tracker-archive-" + AppDateTime.UtcNow.ToString("yyyyMMddTHHmmss") + ".json"); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Account/GetRoute.cs b/code/api/src/Endpoints/Internal/Account/GetRoute.cs index 1aa7ecb..8d6c50f 100644 --- a/code/api/src/Endpoints/Internal/Account/GetRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/GetRoute.cs @@ -2,10 +2,10 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<LoggedInUserModel> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public GetAccountRoute(AppDbContext context) { - _context = context; + public GetAccountRoute(MainAppDatabase database) { + _database = database; } /// <summary> @@ -15,7 +15,7 @@ public class GetAccountRoute : RouteBaseAsync.WithoutRequest.WithActionResult<Lo /// <returns></returns> [HttpGet("~/_/account")] public override async Task<ActionResult<LoggedInUserModel>> HandleAsync(CancellationToken cancellationToken = default) { - var user = _context.Users + var user = _database.Users .Select(x => new {x.Username, x.Id}) .SingleOrDefault(c => c.Id == LoggedInUser.Id); if (user != default) { diff --git a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs index e4ef54c..696c3c2 100644 --- a/code/api/src/Endpoints/Internal/Account/LoginRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/LoginRoute.cs @@ -1,37 +1,34 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; -public class LoginRoute : RouteBaseAsync - .WithRequest<LoginPayload> - .WithActionResult +public class LoginRoute : RouteBaseAsync.WithRequest<LoginPayload>.WithActionResult { - private readonly AppDbContext _context; - private readonly UserService _userService; + private readonly MainAppDatabase _database; + private readonly UserService _userService; - /// <inheritdoc /> - public LoginRoute(AppDbContext context, UserService userService) { - _context = context; - _userService = userService; - } + public LoginRoute(MainAppDatabase database, UserService userService) { + _database = database; + _userService = userService; + } - /// <summary> - /// Login a user. - /// </summary> - /// <param name="request"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [AllowAnonymous] - [HttpPost("~/_/account/login")] - public override async Task<ActionResult> HandleAsync(LoginPayload request, CancellationToken cancellationToken = default) { - if (!ModelState.IsValid) { - return BadRequest(ModelState); - } + /// <summary> + /// Login a user. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpPost("~/_/account/login")] + public override async Task<ActionResult> 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 KnownProblemModel("Invalid username or password")); - } + var user = _database.Users.SingleOrDefault(u => u.Username == request.Username); + if (user == default || !user.VerifyPassword(request.Password)) { + return BadRequest(new KnownProblemModel("Invalid username or password")); + } - await _userService.LogInUser(HttpContext, user, request.Persist); - return Ok(); - } -} + await _userService.LogInUser(HttpContext, user, request.Persist); + 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 index 4a06f4a..042d729 100644 --- a/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/LogoutRoute.cs @@ -2,21 +2,21 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class LogoutRoute : RouteBaseAsync.WithoutRequest.WithActionResult { - private readonly UserService _userService; + private readonly UserService _userService; - public LogoutRoute(UserService userService) { - _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); - return Ok(); - } -} + /// <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); + 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 index 31ff10b..02dc3f1 100644 --- a/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs +++ b/code/api/src/Endpoints/Internal/Account/UpdateAccountRoute.cs @@ -2,50 +2,49 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Account; public class UpdateAccountRoute : RouteBaseAsync.WithRequest<UpdatePayload>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public UpdateAccountRoute(AppDbContext context) { - _context = context; - } + public UpdateAccountRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Update the logged on user's data. - /// </summary> - /// <param name="request"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [HttpPost("~/_/account/update")] - public override async Task<ActionResult> HandleAsync(UpdatePayload request, CancellationToken cancellationToken = default) { - var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); - if (user == default) { - await HttpContext.SignOutAsync(); - return Unauthorized(); - } + /// <summary> + /// Update the logged on user's data. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [HttpPost("~/_/account/update")] + public override async Task<ActionResult> HandleAsync(UpdatePayload 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 BadRequest(new KnownProblemModel("Invalid request", "No data was submitted")); - } + if (request.Password.IsNullOrWhiteSpace() && request.Username.IsNullOrWhiteSpace()) { + return BadRequest(new KnownProblemModel("Invalid request", "No data was submitted")); + } - if (request.Password.HasValue() && request.Password.Length < 6) { - return BadRequest(new KnownProblemModel("Invalid request", - "The new password must contain at least 6 characters")); - } + if (request.Password.HasValue() && request.Password.Length < 6) { + return BadRequest(new KnownProblemModel("Invalid request", + "The new password must contain at least 6 characters")); + } - if (request.Password.HasValue()) { - user.HashAndSetPassword(request.Password); - } + if (request.Password.HasValue()) { + user.HashAndSetPassword(request.Password); + } - if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { - return BadRequest(new KnownProblemModel("Invalid request", - "The new username does not look like a valid email address")); - } + if (request.Username.HasValue() && !request.Username.IsValidEmailAddress()) { + return BadRequest(new KnownProblemModel("Invalid request", + "The new username does not look like a valid email address")); + } - if (request.Username.HasValue()) { - user.Username = request.Username.Trim(); - } + if (request.Username.HasValue()) { + user.Username = request.Username.Trim(); + } - await _context.SaveChangesAsync(cancellationToken); - return Ok(); - } -} + await _database.SaveChangesAsync(cancellationToken); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/RequestModel.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestPayload.cs index 236c650..1adb344 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/RequestModel.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestPayload.cs @@ -1,6 +1,6 @@ -namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests.Create; +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; -public class RequestModel +public class CreateResetRequestPayload { public string Username { get; set; } }
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/Route.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs index f837fc0..bb72d38 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/Create/Route.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/CreateResetRequestRoute.cs @@ -1,15 +1,15 @@ -namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests.Create; +namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; -public class Route : RouteBaseAsync.WithRequest<RequestModel>.WithActionResult +public class Route : RouteBaseAsync.WithRequest<CreateResetRequestPayload>.WithActionResult { private readonly ILogger<Route> _logger; private readonly PasswordResetService _passwordResetService; - private readonly AppDbContext _context; - - public Route(ILogger<Route> logger, PasswordResetService passwordResetService, AppDbContext context) { + private readonly MainAppDatabase _database; + + public Route(ILogger<Route> logger, PasswordResetService passwordResetService, MainAppDatabase database) { _logger = logger; _passwordResetService = passwordResetService; - _context = context; + _database = database; } /// <summary> @@ -20,30 +20,19 @@ public class Route : RouteBaseAsync.WithRequest<RequestModel>.WithActionResult /// <returns></returns> [AllowAnonymous] [HttpPost("~/_/password-reset-request/create")] - public override async Task<ActionResult> HandleAsync(RequestModel request, CancellationToken cancellationToken = default) { + public override async Task<ActionResult> HandleAsync(CreateResetRequestPayload request, CancellationToken cancellationToken = default) { if (!request.Username.IsValidEmailAddress()) { _logger.LogInformation("Username is invalid, not doing request for password change"); return KnownProblem("Invalid email address", request.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(AppDateTime.UtcNow)) { - offset++; - } - - _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours"); - var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz); - _logger.LogInformation("Creating forgot password request with date time: " + requestDateTime.ToString("u")); + var tz = GetRequestTimeZone(_logger); + _logger.LogInformation("Creating forgot password request with local date time: " + tz.LocalDateTime.ToString("u")); try { - var user = _context.Users.SingleOrDefault(c => c.Username.Equals(request.Username)); + var user = _database.Users.SingleOrDefault(c => c.Username.Equals(request.Username)); if (user != default) { - await _passwordResetService.AddRequestAsync(user, tz, cancellationToken); + await _passwordResetService.AddRequestAsync(user, tz.TimeZoneInfo, cancellationToken); return Ok(); } diff --git a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs index a0ad4d0..6f71b2f 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/FulfillResetRequestRoute.cs @@ -1,34 +1,31 @@ - namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; -/// <inheritdoc /> public class FulfillResetRequestRoute : RouteBaseAsync.WithRequest<FulfillResetRequestPayload>.WithActionResult { - private readonly PasswordResetService _passwordResetService; + private readonly PasswordResetService _passwordResetService; - /// <inheritdoc /> - public FulfillResetRequestRoute(PasswordResetService passwordResetService) { - _passwordResetService = passwordResetService; - } + public FulfillResetRequestRoute(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } - /// <summary> - /// Fulfill a password reset request. - /// </summary> - /// <param name="request"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [AllowAnonymous] - [HttpPost("~/_/password-reset-request/fulfill")] - public override async Task<ActionResult> HandleAsync(FulfillResetRequestPayload request, CancellationToken cancellationToken = default) { - try { - var fulfilled = await _passwordResetService.FullFillRequestAsync(request.Id, request.NewPassword, cancellationToken); - return Ok(fulfilled); - } catch (Exception e) { - if (e is ForgotPasswordRequestNotFoundException or UserNotFoundException) { - return NotFound(); - } + /// <summary> + /// Fulfill a password reset request. + /// </summary> + /// <param name="request"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpPost("~/_/password-reset-request/fulfill")] + public override async Task<ActionResult> HandleAsync(FulfillResetRequestPayload request, CancellationToken cancellationToken = default) { + try { + var fulfilled = await _passwordResetService.FullFillRequestAsync(request.Id, request.NewPassword, cancellationToken); + return Ok(fulfilled); + } catch (Exception e) { + if (e is ForgotPasswordRequestNotFoundException or UserNotFoundException) { + return NotFound(); + } - throw; - } - } -} + throw; + } + } +}
\ 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 index 917c4f0..687cef6 100644 --- a/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs +++ b/code/api/src/Endpoints/Internal/PasswordResetRequests/IsResetRequestValidRoute.cs @@ -1,29 +1,27 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.PasswordResetRequests; -/// <inheritdoc /> public class IsResetRequestValidRoute : RouteBaseAsync.WithRequest<Guid>.WithActionResult { - private readonly PasswordResetService _passwordResetService; + private readonly PasswordResetService _passwordResetService; - /// <inheritdoc /> - public IsResetRequestValidRoute(PasswordResetService passwordResetService) { - _passwordResetService = passwordResetService; - } + public IsResetRequestValidRoute(PasswordResetService passwordResetService) { + _passwordResetService = passwordResetService; + } - /// <summary> - /// Check if a given password reset request is still valid. - /// </summary> - /// <param name="id"></param> - /// <param name="cancellationToken"></param> - /// <returns></returns> - [AllowAnonymous] - [HttpGet("~/_/password-reset-request/is-valid")] - public override async Task<ActionResult> HandleAsync(Guid id, CancellationToken cancellationToken = default) { - var request = await _passwordResetService.GetRequestAsync(id, cancellationToken); - if (request == default) { - return NotFound(); - } + /// <summary> + /// Check if a given password reset request is still valid. + /// </summary> + /// <param name="id"></param> + /// <param name="cancellationToken"></param> + /// <returns></returns> + [AllowAnonymous] + [HttpGet("~/_/password-reset-request/is-valid")] + public override async Task<ActionResult> HandleAsync(Guid id, CancellationToken cancellationToken = default) { + var request = await _passwordResetService.GetRequestAsync(id, cancellationToken); + if (request == default) { + return NotFound(); + } - return Ok(request.IsExpired == false); - } -} + return Ok(request.IsExpired == false); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs index 5fb8213..34180d1 100644 --- a/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/GetApplicationVersionRoute.cs @@ -2,20 +2,19 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class GetApplicationVersionRoute : RouteBaseSync.WithoutRequest.WithActionResult<string> { - private readonly IWebHostEnvironment _environment; + private readonly IWebHostEnvironment _environment; - /// <inheritdoc /> - public GetApplicationVersionRoute(IWebHostEnvironment environment) { - _environment = environment; - } + public GetApplicationVersionRoute(IWebHostEnvironment environment) { + _environment = environment; + } - /// <summary> - /// Get the running api version number. - /// </summary> - /// <returns></returns> - [HttpGet("~/_/version")] - public override ActionResult<string> Handle() { - var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt"); - return Ok(System.IO.File.ReadAllText(versionFilePath)); - } -} + /// <summary> + /// Get the running api version number. + /// </summary> + /// <returns></returns> + [HttpGet("~/_/version")] + public override ActionResult<string> Handle() { + var versionFilePath = Path.Combine(_environment.WebRootPath, "version.txt"); + return Ok(System.IO.File.ReadAllText(versionFilePath)); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/Internal/Root/LogRoute.cs b/code/api/src/Endpoints/Internal/Root/LogRoute.cs index 48b497a..2c69e94 100644 --- a/code/api/src/Endpoints/Internal/Root/LogRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/LogRoute.cs @@ -2,15 +2,15 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class LogRoute : RouteBaseSync.WithRequest<string>.WithoutResult { - private readonly ILogger<LogRoute> _logger; + private readonly ILogger<LogRoute> _logger; - public LogRoute(ILogger<LogRoute> logger) { - _logger = logger; - } + public LogRoute(ILogger<LogRoute> logger) { + _logger = logger; + } - [AllowAnonymous] - [HttpPost("~/_/log")] - public override void Handle([FromBody] string request) { - _logger.LogInformation(request); - } -} + [AllowAnonymous] + [HttpPost("~/_/log")] + public override void Handle([FromBody] string request) { + _logger.LogInformation(request); + } +}
\ 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 index e0dcca3..7270fd8 100644 --- a/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/ReadConfigurationRoute.cs @@ -2,16 +2,16 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class ReadConfigurationRoute : RouteBaseSync.WithoutRequest.WithActionResult { - private readonly VaultService _vaultService; + private readonly VaultService _vaultService; - public ReadConfigurationRoute(VaultService vaultService) { - _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"); - } -} + [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 index 4b1beec..a616c00 100644 --- a/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs +++ b/code/api/src/Endpoints/Internal/Root/RefreshConfigurationRoute.cs @@ -2,14 +2,14 @@ namespace IOL.GreatOffice.Api.Endpoints.Internal.Root; public class RefreshConfigurationRoute : RouteBaseSync.WithoutRequest.WithoutResult { - private readonly VaultService _vaultService; + private readonly VaultService _vaultService; - public RefreshConfigurationRoute(VaultService vaultService) { - _vaultService = vaultService; - } + public RefreshConfigurationRoute(VaultService vaultService) { + _vaultService = vaultService; + } - [HttpGet("~/_/refresh-configuration")] - public override void Handle() { - _vaultService.RefreshCurrentAppConfiguration(); - } -} + [HttpGet("~/_/refresh-configuration")] + public override void Handle() { + _vaultService.RefreshCurrentAppConfiguration(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs index 60b00ff..6bc2fdc 100644 --- a/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs +++ b/code/api/src/Endpoints/V1/ApiTokens/CreateTokenRoute.cs @@ -4,13 +4,12 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAccessTokenDto>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; private readonly AppConfiguration _configuration; private readonly ILogger<CreateTokenRoute> _logger; - public CreateTokenRoute(AppDbContext context, VaultService vaultService, ILogger<CreateTokenRoute> logger) - { - _context = context; + public CreateTokenRoute(MainAppDatabase database, VaultService vaultService, ILogger<CreateTokenRoute> logger) { + _database = database; _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger; } @@ -24,24 +23,19 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce [HttpPost("~/v{version:apiVersion}/api-tokens/create")] [ProducesResponseType(200, Type = typeof(string))] [ProducesResponseType(404, Type = typeof(KnownProblemModel))] - public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) - { - var user = _context.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); - if (user == default) - { + public override ActionResult Handle(ApiAccessToken.ApiAccessTokenDto request) { + var user = _database.Users.SingleOrDefault(c => c.Id == LoggedInUser.Id); + if (user == default) { return NotFound(new KnownProblemModel("User does not exist")); } var token_entropy = _configuration.APP_AES_KEY; - if (token_entropy.IsNullOrWhiteSpace()) - { + if (token_entropy.IsNullOrWhiteSpace()) { _logger.LogWarning("No token entropy is available, Basic auth is disabled"); return NotFound(); } - var access_token = new ApiAccessToken() - { - Id = Guid.NewGuid(), + var accessToken = new ApiAccessToken() { User = user, ExpiryDate = request.ExpiryDate.ToUniversalTime(), AllowCreate = request.AllowCreate, @@ -50,8 +44,8 @@ public class CreateTokenRoute : RouteBaseSync.WithRequest<ApiAccessToken.ApiAcce AllowUpdate = request.AllowUpdate }; - _context.AccessTokens.Add(access_token); - _context.SaveChanges(); - return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(access_token.Id.ToString().EncryptWithAes(token_entropy)))); + _database.AccessTokens.Add(accessToken); + _database.SaveChanges(); + return Ok(Convert.ToBase64String(Encoding.UTF8.GetBytes(accessToken.Id.ToString().EncryptWithAes(token_entropy)))); } -} +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs index a90b4c0..116a814 100644 --- a/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs +++ b/code/api/src/Endpoints/V1/ApiTokens/DeleteTokenRoute.cs @@ -2,32 +2,32 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; public class DeleteTokenRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult { - private readonly AppDbContext _context; - private readonly ILogger<DeleteTokenRoute> _logger; + private readonly MainAppDatabase _database; + private readonly ILogger<DeleteTokenRoute> _logger; - public DeleteTokenRoute(AppDbContext context, ILogger<DeleteTokenRoute> logger) { - _context = context; - _logger = logger; - } + public DeleteTokenRoute(MainAppDatabase database, ILogger<DeleteTokenRoute> logger) { + _database = database; + _logger = logger; + } - /// <summary> - /// Delete an api token, rendering it unusable - /// </summary> - /// <param name="id">Id of the token to delete</param> - /// <returns>Nothing</returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public override ActionResult Handle(Guid id) { - var token = _context.AccessTokens.SingleOrDefault(c => c.Id == id); - if (token == default) { - _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received."); - return NotFound(); - } + /// <summary> + /// Delete an api token, rendering it unusable + /// </summary> + /// <param name="id">Id of the token to delete</param> + /// <returns>Nothing</returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpDelete("~/v{version:apiVersion}/api-tokens/delete")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public override ActionResult Handle(Guid id) { + var token = _database.AccessTokens.SingleOrDefault(c => c.Id == id); + if (token == default) { + _logger.LogWarning("A deletion request of an already deleted (maybe) api token was received."); + return NotFound(); + } - _context.AccessTokens.Remove(token); - _context.SaveChanges(); - return Ok(); - } -} + _database.AccessTokens.Remove(token); + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs index 59fd077..19790e4 100644 --- a/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs +++ b/code/api/src/Endpoints/V1/ApiTokens/GetTokensRoute.cs @@ -2,21 +2,21 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.ApiTokens; public class GetTokensRoute : RouteBaseSync.WithoutRequest.WithResult<ActionResult<List<ApiAccessToken.ApiAccessTokenDto>>> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public GetTokensRoute(AppDbContext context) { - _context = context; - } + public GetTokensRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Get all tokens, both active and inactive. - /// </summary> - /// <returns>A list of tokens</returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [HttpGet("~/v{version:apiVersion}/api-tokens")] - [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))] - [ProducesResponseType(204)] - public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() { - return Ok(_context.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto)); - } -} + /// <summary> + /// Get all tokens, both active and inactive. + /// </summary> + /// <returns>A list of tokens</returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [HttpGet("~/v{version:apiVersion}/api-tokens")] + [ProducesResponseType(200, Type = typeof(List<ApiAccessToken.ApiAccessTokenDto>))] + [ProducesResponseType(204)] + public override ActionResult<List<ApiAccessToken.ApiAccessTokenDto>> Handle() { + return Ok(_database.AccessTokens.Where(c => c.User.Id == LoggedInUser.Id).Select(c => c.AsDto)); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs index fac2b5e..0471637 100644 --- a/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs +++ b/code/api/src/Endpoints/V1/Categories/CreateCategoryRoute.cs @@ -2,42 +2,43 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; public class CreateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult<TimeCategory.TimeCategoryDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public CreateCategoryRoute(AppDbContext context) { - _context = context; - } + public CreateCategoryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Create a new time entry category. - /// </summary> - /// <param name="categoryTimeCategoryDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] - [HttpPost("~/v{version:apiVersion}/categories/create")] - [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))] - public override ActionResult<TimeCategory.TimeCategoryDto> Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { - var duplicate = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); - if (duplicate) { - var category = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); - if (category != default) { - return Ok(category.AsDto); - } - } + /// <summary> + /// Create a new time entry category. + /// </summary> + /// <param name="categoryTimeCategoryDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [HttpPost("~/v{version:apiVersion}/categories/create")] + [ProducesResponseType(200, Type = typeof(TimeCategory.TimeCategoryDto))] + public override ActionResult<TimeCategory.TimeCategoryDto> Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { + var duplicate = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .Any(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); + if (duplicate) { + var category = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Name.Trim() == categoryTimeCategoryDto.Name.Trim()); + if (category != default) { + return Ok(category.AsDto); + } + } - var newCategory = new TimeCategory(LoggedInUser.Id) { - Name = categoryTimeCategoryDto.Name.Trim(), - Color = categoryTimeCategoryDto.Color - }; + var newCategory = new TimeCategory(LoggedInUser) { + Name = categoryTimeCategoryDto.Name.Trim(), + Color = categoryTimeCategoryDto.Color + }; + newCategory.SetOwnerIds(LoggedInUser.Id); - _context.TimeCategories.Add(newCategory); - _context.SaveChanges(); - categoryTimeCategoryDto.Id = newCategory.Id; - return Ok(categoryTimeCategoryDto); - } -} + _database.TimeCategories.Add(newCategory); + _database.SaveChanges(); + categoryTimeCategoryDto.Id = newCategory.Id; + return Ok(categoryTimeCategoryDto); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs index 3d438a0..582ec7d 100644 --- a/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs +++ b/code/api/src/Endpoints/V1/Categories/DeleteCategoryRoute.cs @@ -2,37 +2,37 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; public class DeleteCategoryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public DeleteCategoryRoute(AppDbContext context) { - _context = context; - } + public DeleteCategoryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Delete a time entry category. - /// </summary> - /// <param name="id"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] - [HttpDelete("~/v{version:apiVersion}/categories/{id:guid}/delete")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - public override ActionResult Handle(Guid id) { - var category = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == id); + /// <summary> + /// Delete a time entry category. + /// </summary> + /// <param name="id"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/categories/{id:guid}/delete")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public override ActionResult Handle(Guid id) { + var category = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); - if (category == default) { - return NotFound(); - } + if (category == default) { + return NotFound(); + } - var entries = _context.TimeEntries - .Include(c => c.Category) - .Where(c => c.Category.Id == category.Id); - _context.TimeEntries.RemoveRange(entries); - _context.TimeCategories.Remove(category); - _context.SaveChanges(); - return Ok(); - } -} + var entries = _database.TimeEntries + .Include(c => c.Category) + .Where(c => c.Category.Id == category.Id); + _database.TimeEntries.RemoveRange(entries); + _database.TimeCategories.Remove(category); + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs index a40a832..937f8e3 100644 --- a/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs +++ b/code/api/src/Endpoints/V1/Categories/GetCategoriesRoute.cs @@ -1,35 +1,33 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; -/// <inheritdoc /> public class GetCategoriesRoute : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeCategory.TimeCategoryDto>> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public GetCategoriesRoute(AppDbContext context) { - _context = context; - } + public GetCategoriesRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Get a minimal list of time entry categories. - /// </summary> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))] - [ProducesResponseType(204)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] - [HttpGet("~/v{version:apiVersion}/categories")] - public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() { - var categories = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .OrderByDescending(c => c.CreatedAt) - .Select(c => c.AsDto) - .ToList(); + /// <summary> + /// Get a minimal list of time entry categories. + /// </summary> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [ProducesResponseType(200, Type = typeof(List<TimeCategory.TimeCategoryDto>))] + [ProducesResponseType(204)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/categories")] + public override ActionResult<List<TimeCategory.TimeCategoryDto>> Handle() { + var categories = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .OrderByDescending(c => c.CreatedAt) + .Select(c => c.AsDto) + .ToList(); - if (categories.Count == 0) { - return NoContent(); - } + if (categories.Count == 0) { + return NoContent(); + } - return Ok(categories); - } -} + return Ok(categories); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs index ca7dfdf..096132d 100644 --- a/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs +++ b/code/api/src/Endpoints/V1/Categories/UpdateCategoryRoute.cs @@ -2,38 +2,38 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Categories; public class UpdateCategoryRoute : RouteBaseSync.WithRequest<TimeCategory.TimeCategoryDto>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public UpdateCategoryRoute(AppDbContext context) { - _context = context; - } + public UpdateCategoryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Update a time entry category. - /// </summary> - /// <param name="categoryTimeCategoryDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] - [HttpPost("~/v{version:apiVersion}/categories/update")] - [ProducesResponseType(200)] - [ProducesResponseType(404)] - [ProducesResponseType(403)] - public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { - var category = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == categoryTimeCategoryDto.Id); - if (category == default) { - return NotFound(); - } + /// <summary> + /// Update a time entry category. + /// </summary> + /// <param name="categoryTimeCategoryDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/categories/update")] + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesResponseType(403)] + public override ActionResult Handle(TimeCategory.TimeCategoryDto categoryTimeCategoryDto) { + var category = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == categoryTimeCategoryDto.Id); + if (category == default) { + return NotFound(); + } - if (LoggedInUser.Id != category.UserId) { - return Forbid(); - } + if (LoggedInUser.Id != category.UserId) { + return Forbid(); + } - category.Name = categoryTimeCategoryDto.Name; - category.Color = categoryTimeCategoryDto.Color; - _context.SaveChanges(); - return Ok(); - } -} + category.Name = categoryTimeCategoryDto.Name; + category.Color = categoryTimeCategoryDto.Color; + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs new file mode 100644 index 0000000..eb69f7f --- /dev/null +++ b/code/api/src/Endpoints/V1/Customers/CreateCustomerRoute.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.Localization; + +namespace IOL.GreatOffice.Api.Endpoints.V1.Customers; + +public class CreateCustomerRoute : RouteBaseAsync.WithRequest<CreateCustomerPayload>.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly ILogger<CreateCustomerRoute> _logger; + private readonly IStringLocalizer<SharedResources> _localizer; + + public CreateCustomerRoute(MainAppDatabase database, ILogger<CreateCustomerRoute> logger, IStringLocalizer<SharedResources> localizer) { + _database = database; + _logger = logger; + _localizer = localizer; + } + + [HttpPost("~/v{version:apiVersion}/customers/create")] + public override async Task<ActionResult> HandleAsync(CreateCustomerPayload request, CancellationToken cancellationToken = default) { + var errors = new Dictionary<string, string>(); + if (request.Name.Trim().IsNullOrEmpty()) errors.Add("name", _localizer["Name is a required field"]); + if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors); + var customer = new Customer(LoggedInUser) { + CustomerNumber = request.CustomerNumber, + Name = request.Name, + Description = request.Description, + Address1 = request.Address1, + Address2 = request.Address2, + Country = request.Country, + Currency = request.Currency, + Email = request.Email, + Phone = request.Phone, + PostalCity = request.PostalCity, + PostalCode = request.PostalCode, + VATNumber = request.VATNumber, + ORGNumber = request.ORGNumber, + DefaultReference = request.DefaultReference, + Website = request.Website + }; + customer.SetOwnerIds(default, LoggedInUser.TenantId); + _database.Customers.Add(customer); + await _database.SaveChangesAsync(cancellationToken); + return Ok(); + } +} + +public class CreateCustomerPayload +{ + public string CustomerNumber { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string PostalCode { get; set; } + public string PostalCity { get; set; } + public string Country { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public string VATNumber { get; set; } + public string ORGNumber { get; set; } + public string DefaultReference { get; set; } + public string Website { get; set; } + public string Currency { get; set; } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs index 854ff59..45d8f32 100644 --- a/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs +++ b/code/api/src/Endpoints/V1/Entries/CreateEntryRoute.cs @@ -2,65 +2,66 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; public class CreateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public CreateEntryRoute(AppDbContext context) { - _context = context; - } + public CreateEntryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Create a time entry. - /// </summary> - /// <param name="timeEntryTimeEntryDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] - [ProducesResponseType(200)] - [ProducesResponseType(400, Type = typeof(KnownProblemModel))] - [ProducesResponseType(404, Type = typeof(KnownProblemModel))] - [HttpPost("~/v{version:apiVersion}/entries/create")] - public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { - if (timeEntryTimeEntryDto.Stop == default) { - return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required")); - } + /// <summary> + /// Create a time entry. + /// </summary> + /// <param name="timeEntryTimeEntryDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [ProducesResponseType(200)] + [ProducesResponseType(400, Type = typeof(KnownProblemModel))] + [ProducesResponseType(404, Type = typeof(KnownProblemModel))] + [HttpPost("~/v{version:apiVersion}/entries/create")] + public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { + if (timeEntryTimeEntryDto.Stop == default) { + return BadRequest(new KnownProblemModel("Invalid form", "A stop date is required")); + } - if (timeEntryTimeEntryDto.Start == default) { - return BadRequest(new KnownProblemModel("Invalid form", "A start date is required")); - } + if (timeEntryTimeEntryDto.Start == default) { + return BadRequest(new KnownProblemModel("Invalid form", "A start date is required")); + } - if (timeEntryTimeEntryDto.Category == default) { - return BadRequest(new KnownProblemModel("Invalid form", "A category is required")); - } + if (timeEntryTimeEntryDto.Category == default) { + return BadRequest(new KnownProblemModel("Invalid form", "A category is required")); + } - var category = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); - - if (category == default) { - return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); - } + var category = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); - var entry = new TimeEntry(LoggedInUser.Id) { - Category = category, - Start = timeEntryTimeEntryDto.Start.ToUniversalTime(), - Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(), - Description = timeEntryTimeEntryDto.Description, - }; + if (category == default) { + return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } - if (timeEntryTimeEntryDto.Labels?.Count > 0) { - var labels = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id)) - .ToList(); - if (labels.Count != timeEntryTimeEntryDto.Labels.Count) { - return NotFound(new KnownProblemModel("Not found", "Could not find all of the specified labels")); - } + var entry = new TimeEntry(LoggedInUser) { + Category = category, + Start = timeEntryTimeEntryDto.Start.ToUniversalTime(), + Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(), + Description = timeEntryTimeEntryDto.Description, + }; + entry.SetOwnerIds(LoggedInUser.Id); - entry.Labels = labels; - } + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .Where(c => timeEntryTimeEntryDto.Labels.Select(p => p.Id).Contains(c.Id)) + .ToList(); + if (labels.Count != timeEntryTimeEntryDto.Labels.Count) { + return NotFound(new KnownProblemModel("Not found", "Could not find all of the specified labels")); + } - _context.TimeEntries.Add(entry); - _context.SaveChanges(); - return Ok(entry.AsDto); - } -} + entry.Labels = labels; + } + + _database.TimeEntries.Add(entry); + _database.SaveChanges(); + return Ok(entry.AsDto); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs index 0850af0..c488ed5 100644 --- a/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs +++ b/code/api/src/Endpoints/V1/Entries/DeleteEntryRoute.cs @@ -1,35 +1,33 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; -/// <inheritdoc /> public class DeleteEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public DeleteEntryRoute(AppDbContext context) { - _context = context; - } + public DeleteEntryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Delete a time entry. - /// </summary> - /// <param name="id"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] - [HttpDelete("~/v{version:apiVersion}/entries/{id:guid}/delete")] - [ProducesResponseType(404)] - [ProducesResponseType(200)] - public override ActionResult Handle(Guid id) { - var entry = _context.TimeEntries - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == id); - if (entry == default) { - return NotFound(); - } + /// <summary> + /// Delete a time entry. + /// </summary> + /// <param name="id"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/entries/{id:guid}/delete")] + [ProducesResponseType(404)] + [ProducesResponseType(200)] + public override ActionResult Handle(Guid id) { + var entry = _database.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); + if (entry == default) { + return NotFound(); + } - _context.TimeEntries.Remove(entry); - _context.SaveChanges(); - return Ok(); - } -} + _database.TimeEntries.Remove(entry); + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs index ec6003c..03a64d2 100644 --- a/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs +++ b/code/api/src/Endpoints/V1/Entries/EntryQueryRoute.cs @@ -2,185 +2,185 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; public class EntryQueryRoute : RouteBaseSync.WithRequest<EntryQueryPayload>.WithActionResult<EntryQueryResponse> { - private readonly ILogger<EntryQueryRoute> _logger; - private readonly AppDbContext _context; + private readonly ILogger<EntryQueryRoute> _logger; + private readonly MainAppDatabase _database; - public EntryQueryRoute(ILogger<EntryQueryRoute> logger, AppDbContext context) { - _logger = logger; - _context = context; - } + public EntryQueryRoute(ILogger<EntryQueryRoute> logger, MainAppDatabase database) { + _logger = logger; + _database = database; + } - /// <summary> - /// Get a list of entries based on a given query. - /// </summary> - /// <param name="entryQuery"></param> - /// <returns></returns> - /// <exception cref="ArgumentOutOfRangeException"></exception> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] - [HttpPost("~/v{version:apiVersion}/entries/query")] - [ProducesResponseType(204)] - [ProducesResponseType(400, Type = typeof(KnownProblemModel))] - [ProducesResponseType(200, Type = typeof(EntryQueryResponse))] - public override ActionResult<EntryQueryResponse> Handle(EntryQueryPayload entryQuery) { - var result = new TimeQueryDto(); + /// <summary> + /// Get a list of entries based on a given query. + /// </summary> + /// <param name="entryQuery"></param> + /// <returns></returns> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpPost("~/v{version:apiVersion}/entries/query")] + [ProducesResponseType(204)] + [ProducesResponseType(400, Type = typeof(KnownProblemModel))] + [ProducesResponseType(200, Type = typeof(EntryQueryResponse))] + public override ActionResult<EntryQueryResponse> Handle(EntryQueryPayload entryQuery) { + var result = new TimeQueryDto(); - Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader); - var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC"); - var offsetInHours = tz.BaseUtcOffset.Hours; + Request.Headers.TryGetValue(AppHeaders.BROWSER_TIME_ZONE, out var timeZoneHeader); + var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneHeader.ToString().HasValue() ? timeZoneHeader.ToString() : "UTC"); + var offsetInHours = 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(AppDateTime.UtcNow)) { - offsetInHours++; - } + // 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(AppDateTime.UtcNow)) { + offsetInHours++; + } - _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours"); - var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz); - _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u")); + _logger.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offsetInHours + " hours"); + var requestDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz); + _logger.LogInformation("Querying data with date time: " + requestDateTime.ToString("u")); - var skipCount = 0; - if (entryQuery.Page > 1) { - skipCount = entryQuery.PageSize * entryQuery.Page; - } + var skipCount = 0; + if (entryQuery.Page > 1) { + skipCount = entryQuery.PageSize * entryQuery.Page; + } - result.Page = entryQuery.Page; - result.PageSize = entryQuery.PageSize; + result.Page = entryQuery.Page; + result.PageSize = entryQuery.PageSize; - var baseQuery = _context.TimeEntries - .AsNoTracking() - .Include(c => c.Category) - .Include(c => c.Labels) - .Where(c => c.UserId == LoggedInUser.Id) - .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id)) - .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id))) - .OrderByDescending(c => c.Start); + var baseQuery = _database.TimeEntries + .AsNoTracking() + .Include(c => c.Category) + .Include(c => c.Labels) + .Where(c => c.UserId == LoggedInUser.Id) + .ConditionalWhere(entryQuery.Categories?.Any() ?? false, c => entryQuery.Categories.Any(p => p.Id == c.Category.Id)) + .ConditionalWhere(entryQuery.Labels?.Any() ?? false, c => c.Labels.Any(l => entryQuery.Labels.Any(p => p.Id == l.Id))) + .OrderByDescending(c => c.Start); - switch (entryQuery.Duration) { - case TimeEntryQueryDuration.TODAY: - var baseTodaysEntries = baseQuery - .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, AppDateTime.UtcNow.Date) == 0); - var baseTodaysEntriesCount = baseTodaysEntries.Count(); + switch (entryQuery.Duration) { + case TimeEntryQueryDuration.TODAY: + var baseTodaysEntries = baseQuery + .Where(c => DateTime.Compare(c.Start.AddHours(offsetInHours).Date, AppDateTime.UtcNow.Date) == 0); + var baseTodaysEntriesCount = baseTodaysEntries.Count(); - if (baseTodaysEntriesCount == 0) { - return NoContent(); - } + if (baseTodaysEntriesCount == 0) { + return NoContent(); + } - result.TotalSize = baseTodaysEntriesCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseTodaysEntriesCount / entryQuery.PageSize)); + result.TotalSize = baseTodaysEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseTodaysEntriesCount / entryQuery.PageSize)); - var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize); + var pagedTodaysEntries = baseTodaysEntries.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto)); - break; - case TimeEntryQueryDuration.THIS_WEEK: - var lastMonday = AppDateTime.UtcNow.StartOfWeek(DayOfWeek.Monday); + result.Results.AddRange(pagedTodaysEntries.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_WEEK: + var lastMonday = AppDateTime.UtcNow.StartOfWeek(DayOfWeek.Monday); - var baseEntriesThisWeek = baseQuery - .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date); + var baseEntriesThisWeek = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date >= lastMonday.Date && c.Start.AddHours(offsetInHours).Date <= AppDateTime.UtcNow.Date); - var baseEntriesThisWeekCount = baseEntriesThisWeek.Count(); + var baseEntriesThisWeekCount = baseEntriesThisWeek.Count(); - if (baseEntriesThisWeekCount == 0) { - return NoContent(); - } + if (baseEntriesThisWeekCount == 0) { + return NoContent(); + } - result.TotalSize = baseEntriesThisWeekCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisWeekCount / entryQuery.PageSize)); + result.TotalSize = baseEntriesThisWeekCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisWeekCount / entryQuery.PageSize)); - var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize); + var pagedEntriesThisWeek = baseEntriesThisWeek.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto)); - break; - case TimeEntryQueryDuration.THIS_MONTH: - var baseEntriesThisMonth = baseQuery - .Where(c => c.Start.AddHours(offsetInHours).Month == AppDateTime.UtcNow.Month - && c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); - var baseEntriesThisMonthCount = baseEntriesThisMonth.Count(); - if (baseEntriesThisMonthCount == 0) { - return NoContent(); - } + result.Results.AddRange(pagedEntriesThisWeek.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_MONTH: + var baseEntriesThisMonth = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Month == AppDateTime.UtcNow.Month + && c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); + var baseEntriesThisMonthCount = baseEntriesThisMonth.Count(); + if (baseEntriesThisMonthCount == 0) { + return NoContent(); + } - result.TotalSize = baseEntriesThisMonthCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisMonthCount / entryQuery.PageSize)); + result.TotalSize = baseEntriesThisMonthCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisMonthCount / entryQuery.PageSize)); - var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize); + var pagedEntriesThisMonth = baseEntriesThisMonth.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto)); - break; - case TimeEntryQueryDuration.THIS_YEAR: - var baseEntriesThisYear = baseQuery - .Where(c => c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); + result.Results.AddRange(pagedEntriesThisMonth.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.THIS_YEAR: + var baseEntriesThisYear = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Year == AppDateTime.UtcNow.Year); - var baseEntriesThisYearCount = baseEntriesThisYear.Count(); - if (baseEntriesThisYearCount == 0) { - return NoContent(); - } + var baseEntriesThisYearCount = baseEntriesThisYear.Count(); + if (baseEntriesThisYearCount == 0) { + return NoContent(); + } - result.TotalSize = baseEntriesThisYearCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesThisYearCount / entryQuery.PageSize)); + result.TotalSize = baseEntriesThisYearCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesThisYearCount / entryQuery.PageSize)); - var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize); + var pagedEntriesThisYear = baseEntriesThisYear.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto)); - break; - case TimeEntryQueryDuration.SPECIFIC_DATE: - var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc); - var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date); - var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count(); + result.Results.AddRange(pagedEntriesThisYear.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.SPECIFIC_DATE: + var date = DateTime.SpecifyKind(entryQuery.SpecificDate, DateTimeKind.Utc); + var baseEntriesOnThisDate = baseQuery.Where(c => c.Start.AddHours(offsetInHours).Date == date.Date); + var baseEntriesOnThisDateCount = baseEntriesOnThisDate.Count(); - if (baseEntriesOnThisDateCount == 0) { - return NoContent(); - } + if (baseEntriesOnThisDateCount == 0) { + return NoContent(); + } - result.TotalSize = baseEntriesOnThisDateCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseEntriesOnThisDateCount / entryQuery.PageSize)); + result.TotalSize = baseEntriesOnThisDateCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseEntriesOnThisDateCount / entryQuery.PageSize)); - var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize); + var pagedEntriesOnThisDate = baseEntriesOnThisDate.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto)); - break; - case TimeEntryQueryDuration.DATE_RANGE: - if (entryQuery.DateRange.From == default) { - return BadRequest(new KnownProblemModel("Invalid query", "From date cannot be empty")); - } + result.Results.AddRange(pagedEntriesOnThisDate.Select(c => c.AsDto)); + break; + case TimeEntryQueryDuration.DATE_RANGE: + if (entryQuery.DateRange.From == default) { + return BadRequest(new KnownProblemModel("Invalid query", "From date cannot be empty")); + } - var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc); + var fromDate = DateTime.SpecifyKind(entryQuery.DateRange.From, DateTimeKind.Utc); - if (entryQuery.DateRange.To == default) { - return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty")); - } + if (entryQuery.DateRange.To == default) { + return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be empty")); + } - var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc); + var toDate = DateTime.SpecifyKind(entryQuery.DateRange.To, DateTimeKind.Utc); - if (DateTime.Compare(fromDate, toDate) > 0) { - return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be less than From date")); - } + if (DateTime.Compare(fromDate, toDate) > 0) { + return BadRequest(new KnownProblemModel("Invalid query", "To date cannot be less than From date")); + } - var baseDateRangeEntries = baseQuery - .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate); + var baseDateRangeEntries = baseQuery + .Where(c => c.Start.AddHours(offsetInHours).Date > fromDate && c.Start.AddHours(offsetInHours).Date <= toDate); - var baseDateRangeEntriesCount = baseDateRangeEntries.Count(); - if (baseDateRangeEntriesCount == 0) { - return NoContent(); - } + var baseDateRangeEntriesCount = baseDateRangeEntries.Count(); + if (baseDateRangeEntriesCount == 0) { + return NoContent(); + } - result.TotalSize = baseDateRangeEntriesCount; - result.TotalPageCount = Convert.ToInt32(Math.Round((double)baseDateRangeEntriesCount / entryQuery.PageSize)); + result.TotalSize = baseDateRangeEntriesCount; + result.TotalPageCount = Convert.ToInt32(Math.Round((double) baseDateRangeEntriesCount / entryQuery.PageSize)); - var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize); + var pagedDateRangeEntries = baseDateRangeEntries.Skip(skipCount).Take(entryQuery.PageSize); - result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query"); - } + result.Results.AddRange(pagedDateRangeEntries.Select(c => c.AsDto)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(entryQuery), "Unknown duration for query"); + } - if (result.Results.Any() && result.Page == 0) { - result.Page = 1; - result.TotalPageCount = 1; - } + if (result.Results.Any() && result.Page == 0) { + result.Page = 1; + result.TotalPageCount = 1; + } - return Ok(result); - } -} + return Ok(result); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs index 87038db..6e064c7 100644 --- a/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs +++ b/code/api/src/Endpoints/V1/Entries/GetEntryRoute.cs @@ -2,33 +2,33 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; public class GetEntryRoute : RouteBaseSync.WithRequest<Guid>.WithActionResult<TimeEntry.TimeEntryDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public GetEntryRoute(AppDbContext context) { - _context = context; - } + public GetEntryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Get a spesific time entry. - /// </summary> - /// <param name="id"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] - [HttpGet("~/v{version:apiVersion}/entries/{id:guid}")] - [ProducesResponseType(404)] - [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] - public override ActionResult<TimeEntry.TimeEntryDto> Handle(Guid id) { - var entry = _context.TimeEntries - .Where(c => c.UserId == LoggedInUser.Id) - .Include(c => c.Category) - .Include(c => c.Labels) - .SingleOrDefault(c => c.Id == id); + /// <summary> + /// Get a spesific time entry. + /// </summary> + /// <param name="id"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/entries/{id:guid}")] + [ProducesResponseType(404)] + [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] + public override ActionResult<TimeEntry.TimeEntryDto> Handle(Guid id) { + var entry = _database.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .Include(c => c.Category) + .Include(c => c.Labels) + .SingleOrDefault(c => c.Id == id); - if (entry == default) { - return NotFound(); - } + if (entry == default) { + return NotFound(); + } - return Ok(entry); - } -} + return Ok(entry); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs index 09e3b9c..2254214 100644 --- a/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs +++ b/code/api/src/Endpoints/V1/Entries/UpdateEntryRoute.cs @@ -2,65 +2,65 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Entries; public class UpdateEntryRoute : RouteBaseSync.WithRequest<TimeEntry.TimeEntryDto>.WithActionResult<TimeEntry.TimeEntryDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public UpdateEntryRoute(AppDbContext context) { - _context = context; - } + public UpdateEntryRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Update a time entry. - /// </summary> - /// <param name="timeEntryTimeEntryDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] - [HttpPost("~/v{version:apiVersion}/entries/update")] - [ProducesResponseType(404, Type = typeof(KnownProblemModel))] - [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] - public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { - var entry = _context.TimeEntries - .Where(c => c.UserId == LoggedInUser.Id) - .Include(c => c.Labels) - .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id); + /// <summary> + /// Update a time entry. + /// </summary> + /// <param name="timeEntryTimeEntryDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/entries/update")] + [ProducesResponseType(404, Type = typeof(KnownProblemModel))] + [ProducesResponseType(200, Type = typeof(TimeEntry.TimeEntryDto))] + public override ActionResult<TimeEntry.TimeEntryDto> Handle(TimeEntry.TimeEntryDto timeEntryTimeEntryDto) { + var entry = _database.TimeEntries + .Where(c => c.UserId == LoggedInUser.Id) + .Include(c => c.Labels) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Id); - if (entry == default) { - return NotFound(); - } + if (entry == default) { + return NotFound(); + } - var category = _context.TimeCategories - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); - if (category == default) { - return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); - } + var category = _database.TimeCategories + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == timeEntryTimeEntryDto.Category.Id); + if (category == default) { + return NotFound(new KnownProblemModel("Not found", $"Could not find category {timeEntryTimeEntryDto.Category.Name}")); + } - entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime(); - entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(); - entry.Description = timeEntryTimeEntryDto.Description; - entry.Category = category; + entry.Start = timeEntryTimeEntryDto.Start.ToUniversalTime(); + entry.Stop = timeEntryTimeEntryDto.Stop.ToUniversalTime(); + entry.Description = timeEntryTimeEntryDto.Description; + entry.Category = category; - if (timeEntryTimeEntryDto.Labels?.Count > 0) { - var labels = new List<TimeLabel>(); + if (timeEntryTimeEntryDto.Labels?.Count > 0) { + var labels = new List<TimeLabel>(); - foreach (var labelDto in timeEntryTimeEntryDto.Labels) { - var label = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == labelDto.Id); + foreach (var labelDto in timeEntryTimeEntryDto.Labels) { + var label = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == labelDto.Id); - if (label == default) { - continue; - } + if (label == default) { + continue; + } - labels.Add(label); - } + labels.Add(label); + } - entry.Labels = labels; - } else { - entry.Labels = default; - } + entry.Labels = labels; + } else { + entry.Labels = default; + } - _context.SaveChanges(); - return Ok(entry.AsDto); - } -} + _database.SaveChanges(); + return Ok(entry.AsDto); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs index 4fe418b..d6106aa 100644 --- a/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs +++ b/code/api/src/Endpoints/V1/Labels/CreateLabelRoute.cs @@ -1,44 +1,44 @@ - namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; public class CreateLabelRoute : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult<TimeLabel.TimeLabelDto> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - public CreateLabelRoute(AppDbContext context) { - _context = context; - } + public CreateLabelRoute(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Create a time entry label. - /// </summary> - /// <param name="labelTimeLabelDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] - [HttpPost("~/v{version:apiVersion}/labels/create")] - public override ActionResult<TimeLabel.TimeLabelDto> Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { - var duplicate = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); - if (duplicate) { - var label = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); + /// <summary> + /// Create a time entry label. + /// </summary> + /// <param name="labelTimeLabelDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_CREATE)] + [HttpPost("~/v{version:apiVersion}/labels/create")] + public override ActionResult<TimeLabel.TimeLabelDto> Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { + var duplicate = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .Any(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); + if (duplicate) { + var label = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Name.Trim() == labelTimeLabelDto.Name.Trim()); - if (label != default) { - return Ok(label.AsDto); - } - } + if (label != default) { + return Ok(label.AsDto); + } + } - var newLabel = new TimeLabel(LoggedInUser.Id) { - Name = labelTimeLabelDto.Name.Trim(), - Color = labelTimeLabelDto.Color - }; + var newLabel = new TimeLabel(LoggedInUser) { + Name = labelTimeLabelDto.Name.Trim(), + Color = labelTimeLabelDto.Color + }; + newLabel.SetOwnerIds(LoggedInUser.Id); - _context.TimeLabels.Add(newLabel); - _context.SaveChanges(); - labelTimeLabelDto.Id = newLabel.Id; - return Ok(labelTimeLabelDto); - } -} + _database.TimeLabels.Add(newLabel); + _database.SaveChanges(); + labelTimeLabelDto.Id = newLabel.Id; + return Ok(labelTimeLabelDto); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs index d845a6f..1baf4ef 100644 --- a/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs +++ b/code/api/src/Endpoints/V1/Labels/DeleteLabelRoute.cs @@ -1,35 +1,32 @@ - namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; -/// <inheritdoc /> public class DeleteLabelEndpoint : RouteBaseSync.WithRequest<Guid>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public DeleteLabelEndpoint(AppDbContext context) { - _context = context; - } + public DeleteLabelEndpoint(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Delete a time entry label. - /// </summary> - /// <param name="id"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] - [HttpDelete("~/v{version:apiVersion}/labels/{id:guid}/delete")] - public override ActionResult Handle(Guid id) { - var label = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == id); + /// <summary> + /// Delete a time entry label. + /// </summary> + /// <param name="id"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_DELETE)] + [HttpDelete("~/v{version:apiVersion}/labels/{id:guid}/delete")] + public override ActionResult Handle(Guid id) { + var label = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == id); - if (label == default) { - return NotFound(); - } + if (label == default) { + return NotFound(); + } - _context.TimeLabels.Remove(label); - _context.SaveChanges(); - return Ok(); - } -} + _database.TimeLabels.Remove(label); + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs index c9ccef3..09d453b 100644 --- a/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs +++ b/code/api/src/Endpoints/V1/Labels/GetLabelRoute.cs @@ -1,34 +1,31 @@ - namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; -/// <inheritdoc /> public class GetEndpoint : RouteBaseSync.WithoutRequest.WithActionResult<List<TimeLabel.TimeLabelDto>> { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public GetEndpoint(AppDbContext context) { - _context = context; - } + public GetEndpoint(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Get a minimal list of time entry labels. - /// </summary> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] - [HttpGet("~/v{version:apiVersion}/labels")] - public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() { - var labels = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .OrderByDescending(c => c.CreatedAt) - .Select(c => c.AsDto) - .ToList(); + /// <summary> + /// Get a minimal list of time entry labels. + /// </summary> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_READ)] + [HttpGet("~/v{version:apiVersion}/labels")] + public override ActionResult<List<TimeLabel.TimeLabelDto>> Handle() { + var labels = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .OrderByDescending(c => c.CreatedAt) + .Select(c => c.AsDto) + .ToList(); - if (labels.Count == 0) { - return NoContent(); - } + if (labels.Count == 0) { + return NoContent(); + } - return Ok(labels); - } -} + return Ok(labels); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs index 30d72ec..9857b7d 100644 --- a/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs +++ b/code/api/src/Endpoints/V1/Labels/UpdateLabelRoute.cs @@ -1,38 +1,36 @@ namespace IOL.GreatOffice.Api.Endpoints.V1.Labels; -/// <inheritdoc /> public class UpdateLabelEndpoint : RouteBaseSync.WithRequest<TimeLabel.TimeLabelDto>.WithActionResult { - private readonly AppDbContext _context; + private readonly MainAppDatabase _database; - /// <inheritdoc /> - public UpdateLabelEndpoint(AppDbContext context) { - _context = context; - } + public UpdateLabelEndpoint(MainAppDatabase database) { + _database = database; + } - /// <summary> - /// Update a time entry label. - /// </summary> - /// <param name="labelTimeLabelDto"></param> - /// <returns></returns> - [ApiVersion(ApiSpecV1.VERSION_STRING)] - [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] - [HttpPost("~/v{version:apiVersion}/labels/update")] - public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { - var label = _context.TimeLabels - .Where(c => c.UserId == LoggedInUser.Id) - .SingleOrDefault(c => c.Id == labelTimeLabelDto.Id); - if (label == default) { - return NotFound(); - } + /// <summary> + /// Update a time entry label. + /// </summary> + /// <param name="labelTimeLabelDto"></param> + /// <returns></returns> + [ApiVersion(ApiSpecV1.VERSION_STRING)] + [BasicAuthentication(AppConstants.TOKEN_ALLOW_UPDATE)] + [HttpPost("~/v{version:apiVersion}/labels/update")] + public override ActionResult Handle(TimeLabel.TimeLabelDto labelTimeLabelDto) { + var label = _database.TimeLabels + .Where(c => c.UserId == LoggedInUser.Id) + .SingleOrDefault(c => c.Id == labelTimeLabelDto.Id); + if (label == default) { + return NotFound(); + } - if (LoggedInUser.Id != label.UserId) { - return Forbid(); - } + if (LoggedInUser.Id != label.UserId) { + return Forbid(); + } - label.Name = labelTimeLabelDto.Name; - label.Color = labelTimeLabelDto.Color; - _context.SaveChanges(); - return Ok(); - } -} + label.Name = labelTimeLabelDto.Name; + label.Color = labelTimeLabelDto.Color; + _database.SaveChanges(); + return Ok(); + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs new file mode 100644 index 0000000..5c78e27 --- /dev/null +++ b/code/api/src/Endpoints/V1/Projects/CreateProjectRoute.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.Localization; + +namespace IOL.GreatOffice.Api.Endpoints.V1.Projects; + +public class CreateProjectRoute : RouteBaseAsync.WithRequest<CreateProjectPayload>.WithActionResult +{ + private readonly MainAppDatabase _database; + private readonly IStringLocalizer<SharedResources> _localizer; + + public CreateProjectRoute(MainAppDatabase database, IStringLocalizer<SharedResources> localizer) { + _database = database; + _localizer = localizer; + } + + [HttpPost("~/v{version:apiVersion}/projects/create")] + public override async Task<ActionResult> HandleAsync(CreateProjectPayload request, CancellationToken cancellationToken = default) { + var errors = new Dictionary<string, string>(); + + if (request.Name.IsNullOrEmpty()) { + errors.Add("name", _localizer["Name is a required field"]); + } + + var project = new Project(LoggedInUser) { + Name = request.Name, + Description = request.Description, + Start = request.Start, + Stop = request.Stop, + }; + project.SetOwnerIds(default, LoggedInUser.TenantId); + + foreach (var customerId in request.CustomerIds) { + var customer = _database.Customers.FirstOrDefault(c => c.Id == customerId); + if (customer == default) { + errors.Add("customer_" + customerId, _localizer["Customer not found"]); + continue; + } + + project.Customers.Add(customer); + } + + foreach (var member in request.Members) { + var user = _database.Users.FirstOrDefault(c => c.Id == member.UserId); + if (user == default) { + errors.Add("members_" + member.UserId, _localizer["User not found"]); + continue; + } + + project.Members.Add(new ProjectMember() { + Project = project, + User = user, + Role = member.Role + }); + } + + if (errors.Any()) return KnownProblem(_localizer["Invalid form"], _localizer["One or more fields is invalid"], errors); + + _database.Projects.Add(project); + await _database.SaveChangesAsync(cancellationToken); + return Ok(); + } +} + +public class CreateProjectResponse +{ } + +public class CreateProjectPayload +{ + public string Name { get; set; } + public string Description { get; set; } + public DateTime Start { get; set; } + public DateTime Stop { get; set; } + public List<Guid> CustomerIds { get; set; } + public List<ProjectMember> Members { get; set; } + + public class ProjectMember + { + public Guid UserId { get; set; } + public ProjectRole Role { get; set; } + } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs new file mode 100644 index 0000000..d60f974 --- /dev/null +++ b/code/api/src/Endpoints/V1/Projects/GetProjectsRoute.cs @@ -0,0 +1,43 @@ +using MR.AspNetCore.Pagination; + +namespace IOL.GreatOffice.Api.Endpoints.V1.Projects; + +public class GetProjectsRoute : RouteBaseAsync.WithRequest<GetProjectsQueryParameters>.WithActionResult<KeysetPaginationResult<GetProjectsResponseDto>> +{ + private readonly MainAppDatabase _database; + private readonly PaginationService _pagination; + + public GetProjectsRoute(MainAppDatabase database, PaginationService pagination) { + _database = database; + _pagination = pagination; + } + + [HttpGet("~/v{version:apiVersion}/projects")] + public override async Task<ActionResult<KeysetPaginationResult<GetProjectsResponseDto>>> HandleAsync(GetProjectsQueryParameters request, CancellationToken cancellationToken = default) { + var result = await _pagination.KeysetPaginateAsync( + _database.Projects.ForTenant(LoggedInUser), + b => b.Descending(x => x.CreatedAt), + async id => await _database.Projects.FindAsync(id), + query => query.Select(p => new GetProjectsResponseDto() { + Id = p.Id, + Name = p.Name + }) + ); + return Ok(result); + } +} + +public class GetProjectsResponseDto +{ + public Guid Id { get; set; } + public string Name { get; set; } +} + +public class GetProjectsQueryParameters +{ + [FromQuery] + public int Page { get; set; } + + [FromQuery] + public int Size { get; set; } +}
\ No newline at end of file diff --git a/code/api/src/Endpoints/V1/Projects/_calls.http b/code/api/src/Endpoints/V1/Projects/_calls.http new file mode 100644 index 0000000..af0eba6 --- /dev/null +++ b/code/api/src/Endpoints/V1/Projects/_calls.http @@ -0,0 +1,12 @@ +### Create Project +GET https://localhost:5001/v1/projects/create +Accept: application/json +Content-Type: application/json +Cookie: "" + +{ + "": "" +} + +### Get Projects +POST http://localhost diff --git a/code/api/src/Endpoints/V1/RouteBaseAsync.cs b/code/api/src/Endpoints/V1/RouteBaseAsync.cs index a75e9da..33b6f5f 100644 --- a/code/api/src/Endpoints/V1/RouteBaseAsync.cs +++ b/code/api/src/Endpoints/V1/RouteBaseAsync.cs @@ -3,9 +3,9 @@ namespace IOL.GreatOffice.Api.Endpoints.V1; /// <summary> /// A base class for an endpoint that accepts parameters. /// </summary> -public static class RouteBaseAsync +public class RouteBaseAsync { - public static class WithRequest<TRequest> + public class WithRequest<TRequest> { public abstract class WithResult<TResponse> : V1_EndpointBase { |
