diff options
| author | ivar <i@oiee.no> | 2023-11-11 22:10:42 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2023-11-11 22:10:42 +0100 |
| commit | 854dedead3a3ed987997a0132f527db73b65b0ac (patch) | |
| tree | 982dddd8b1dc4c819147912222ec2b38dd3b671e | |
| parent | 7e874b9aecabe22a731d582505cadd87b699d159 (diff) | |
| download | greatoffice-854dedead3a3ed987997a0132f527db73b65b0ac.tar.xz greatoffice-854dedead3a3ed987997a0132f527db73b65b0ac.zip | |
Div more changes
| -rw-r--r-- | code/api/src/Endpoints/EndpointBase.cs | 25 | ||||
| -rw-r--r-- | code/api/src/Models/Static/JsonSettings.cs | 9 | ||||
| -rw-r--r-- | code/api/src/Program.cs | 113 | ||||
| -rw-r--r-- | code/api/src/Services/EmailValidationService.cs | 2 | ||||
| -rw-r--r-- | code/api/src/Services/PasswordResetService.cs | 45 | ||||
| -rw-r--r-- | code/api/src/Services/TenantService.cs | 2 | ||||
| -rw-r--r-- | code/api/src/Services/UserService.cs | 2 | ||||
| -rw-r--r-- | code/api/src/Utilities/BasicAuthenticationAttribute.cs | 30 | ||||
| -rw-r--r-- | code/api/src/Utilities/BasicAuthenticationHandler.cs | 27 | ||||
| -rw-r--r-- | code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs | 6 | ||||
| -rw-r--r-- | code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs | 16 | ||||
| -rwxr-xr-x | code/app/bun.lockb | bin | 119706 -> 232153 bytes | |||
| -rw-r--r-- | code/app/package.json | 17 | ||||
| -rw-r--r-- | code/app/src/utilities/_fetch.ts | 23 |
14 files changed, 180 insertions, 137 deletions
diff --git a/code/api/src/Endpoints/EndpointBase.cs b/code/api/src/Endpoints/EndpointBase.cs index 105fbdf..e8e2494 100644 --- a/code/api/src/Endpoints/EndpointBase.cs +++ b/code/api/src/Endpoints/EndpointBase.cs @@ -8,15 +8,18 @@ public class EndpointBase : ControllerBase /// <summary> /// User data for the currently logged on user. /// </summary> - protected LoggedInUserModel LoggedInUser => new() { + protected LoggedInUserModel LoggedInUser => new() + { Username = User.FindFirstValue(AppClaims.NAME), Id = User.FindFirstValue(AppClaims.USER_ID).AsGuid(), }; [NonAction] - protected ActionResult KnownProblem(string title = default, string subtitle = default, Dictionary<string, string[]> errors = default) { - HttpContext.Response.Headers.Add(AppHeaders.IS_KNOWN_PROBLEM, "1"); - return BadRequest(new KnownProblemModel { + protected ActionResult KnownProblem(string title = default, string subtitle = default, Dictionary<string, string[]> errors = default) + { + HttpContext.Response.Headers.Append(AppHeaders.IS_KNOWN_PROBLEM, "1"); + return BadRequest(new KnownProblemModel + { Title = title, Subtitle = subtitle, Errors = errors, @@ -25,27 +28,31 @@ public class EndpointBase : ControllerBase } [NonAction] - protected ActionResult KnownProblem(KnownProblemModel problem) { - HttpContext.Response.Headers.Add(AppHeaders.IS_KNOWN_PROBLEM, "1"); + protected ActionResult KnownProblem(KnownProblemModel problem) + { + HttpContext.Response.Headers.Append(AppHeaders.IS_KNOWN_PROBLEM, "1"); problem.TraceId = HttpContext.TraceIdentifier; return BadRequest(problem); } [NonAction] - protected RequestTimeZoneInfo GetRequestTimeZone(ILogger logger = default) { + 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)) { + if (tz.IsDaylightSavingTime(AppDateTime.UtcNow)) + { offset++; } logger?.LogInformation("Request time zone (" + tz.Id + ") offset is: " + offset + " hours"); - return new RequestTimeZoneInfo() { + return new RequestTimeZoneInfo() + { TimeZoneInfo = tz, Offset = offset, LocalDateTime = TimeZoneInfo.ConvertTimeFromUtc(AppDateTime.UtcNow, tz) diff --git a/code/api/src/Models/Static/JsonSettings.cs b/code/api/src/Models/Static/JsonSettings.cs index a163c11..3405606 100644 --- a/code/api/src/Models/Static/JsonSettings.cs +++ b/code/api/src/Models/Static/JsonSettings.cs @@ -1,11 +1,16 @@ -namespace IOL.GreatOffice.Api.Data.Static; +namespace IOL.GreatOffice.Api.Models.Static; public static class JsonSettings { - public static Action<JsonOptions> Default { get; } = options => { + public static Action<JsonOptions> SetDefaultAction { get; } = options => + { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; }; + public static readonly JsonSerializerOptions WriteIndented = new() + { + WriteIndented = true + }; } diff --git a/code/api/src/Program.cs b/code/api/src/Program.cs index fcb9465..7277fd3 100644 --- a/code/api/src/Program.cs +++ b/code/api/src/Program.cs @@ -8,11 +8,12 @@ global using System.ComponentModel.DataAnnotations.Schema; global using System.Security.Claims; global using System.Text.Json; global using System.Text.Json.Serialization; -global using IOL.GreatOffice.Api.Data.Database; -global using IOL.GreatOffice.Api.Data.Enums; -global using IOL.GreatOffice.Api.Data.Models; -global using IOL.GreatOffice.Api.Data.Static; +global using IOL.GreatOffice.Api.Models.Database; +global using IOL.GreatOffice.Api.Models.Enums; +global using IOL.GreatOffice.Api.Models.Models; +global using IOL.GreatOffice.Api.Models.Static; global using IOL.GreatOffice.Api.Services; +global using IOL.GreatOffice.Api.Resources; global using IOL.GreatOffice.Api.Utilities; global using IOL.Helpers; global using Microsoft.OpenApi.Models; @@ -31,10 +32,8 @@ global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Serilog; global using Quartz; -global using IOL.GreatOffice.Api.Resources; using IOL.GreatOffice.Api.Endpoints.V1; using IOL.GreatOffice.Api.Jobs; -using IOL.GreatOffice.Api.Models.Database; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc.Versioning; @@ -44,7 +43,9 @@ namespace IOL.GreatOffice.Api; public static class Program { - public static WebApplicationBuilder CreateAppBuilder(string[] args) { + private static readonly string[] supportedCultures = ["en", "nb"]; + public static WebApplicationBuilder CreateAppBuilder(string[] args) + { var builder = WebApplication.CreateBuilder(args); builder.Services.AddLogging(); builder.Services.AddHttpClient(); @@ -67,37 +68,37 @@ public static class Program .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .WriteTo.Console(); - if (!builder.Environment.IsDevelopment() && configuration.SEQ_API_KEY.HasValue() && configuration.SEQ_API_URL.HasValue()) { + if (!builder.Environment.IsDevelopment() && configuration.SEQ_API_KEY.HasValue() && configuration.SEQ_API_URL.HasValue()) + { logger.WriteTo.Seq(configuration.SEQ_API_URL, apiKey: configuration.SEQ_API_KEY); } Log.Logger = logger.CreateLogger(); - Log.Information("Starting web host, " - + JsonSerializer.Serialize(configuration.GetPublicVersion(), - new JsonSerializerOptions() { - WriteIndented = true - })); + Log.Information("Starting web host, " + JsonSerializer.Serialize(configuration.GetPublicObject(), JsonSettings.WriteIndented)); builder.Host.UseSerilog(Log.Logger); - if (builder.Environment.IsDevelopment()) { + if (builder.Environment.IsDevelopment()) + { builder.Services.AddCors(); } - if (builder.Environment.IsProduction()) { + if (builder.Environment.IsProduction()) + { builder.Services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; }); } builder.Services.AddLocalization(); - builder.Services.AddRequestLocalization(options => { - var supportedCultures = new[] {"en", "nb"}; + builder.Services.AddRequestLocalization(options => + { options.SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); options.ApplyCurrentCultureToResponseHeaders = true; }); - builder.Services.Configure<RequestLocalizationOptions>(options => { + builder.Services.Configure<RequestLocalizationOptions>(options => + { options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context => // Get culture from specific cookie await Task.FromResult(new ProviderCultureResult(context.Request.Cookies[AppCookies.Locale] ?? "en"))) @@ -109,61 +110,72 @@ public static class Program .ProtectKeysWithCertificate(configuration.CERT1()) .PersistKeysToDbContext<MainAppDatabase>(); - builder.Services.Configure(JsonSettings.Default); - builder.Services.AddQuartz(options => { - options.UsePersistentStore(o => { + builder.Services.Configure(JsonSettings.SetDefaultAction); + + builder.Services.AddQuartz(options => + { + options.UsePersistentStore(o => + { o.UsePostgres(builder.Configuration.GetQuartzDatabaseConnectionString(vaultService.GetCurrentAppConfiguration)); o.UseSerializer<QuartzJsonSerializer>(); }); - options.UseMicrosoftDependencyInjectionJobFactory(); options.RegisterJobs(); }); builder.Services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; }); - builder.Services.AddAuthentication(options => { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }) - .AddCookie(options => { + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie(options => + { options.Cookie.Name = AppCookies.Session; options.Cookie.Domain = builder.Environment.IsDevelopment() ? "localhost" : ".greatoffice.app"; options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.SlidingExpiration = true; options.Events.OnRedirectToAccessDenied = - options.Events.OnRedirectToLogin = c => { + options.Events.OnRedirectToLogin = c => + { c.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.FromResult<object>(null); }; }) .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AppConstants.BASIC_AUTH_SCHEME, default); - builder.Services.AddDbContext<MainAppDatabase>(options => { + builder.Services.AddDbContext<MainAppDatabase>(options => + { options.UseNpgsql(builder.Configuration.GetAppDatabaseConnectionString(vaultService.GetCurrentAppConfiguration), - npgsqlDbContextOptionsBuilder => { + npgsqlDbContextOptionsBuilder => + { npgsqlDbContextOptionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); npgsqlDbContextOptionsBuilder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), default); }) .UseSnakeCaseNamingConvention(); - if (builder.Environment.IsDevelopment()) { + if (builder.Environment.IsDevelopment()) + { options.EnableSensitiveDataLogging(); } }); - builder.Services.AddApiVersioning(options => { + builder.Services.AddApiVersioning(options => + { options.ApiVersionReader = new UrlSegmentApiVersionReader(); options.ReportApiVersions = true; options.AssumeDefaultVersionWhenUnspecified = false; }); builder.Services.AddVersionedApiExplorer(options => { options.SubstituteApiVersionInUrl = true; }); - builder.Services.AddSwaggerGen(options => { + builder.Services.AddSwaggerGen(options => + { options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "IOL.GreatOffice.Api.xml")); options.UseApiEndpoints(); options.OperationFilter<SwaggerDefaultValues>(); options.OperationFilter<PaginationOperationFilter>(); options.SwaggerDoc(ApiSpecV1.Document.VersionName, ApiSpecV1.Document.OpenApiInfo); options.AddSecurityDefinition("Basic", - new OpenApiSecurityScheme { + new OpenApiSecurityScheme + { Name = "Authorization", Type = SecuritySchemeType.ApiKey, Scheme = "Basic", @@ -186,7 +198,8 @@ public static class Program }); }); - builder.Services.AddPagination(options => { + builder.Services.AddPagination(options => + { options.DefaultSize = 50; options.MaxSize = 100; options.CanChangeSizeFromQuery = true; @@ -195,16 +208,19 @@ public static class Program builder.Services .AddControllers() .AddDataAnnotationsLocalization() - .AddJsonOptions(JsonSettings.Default); + .AddJsonOptions(JsonSettings.SetDefaultAction); return builder; } - public static WebApplication CreateWebApplication(WebApplicationBuilder builder) { + public static WebApplication CreateWebApplication(WebApplicationBuilder builder) + { var app = builder.Build(); - if (app.Environment.IsDevelopment()) { + if (app.Environment.IsDevelopment()) + { app.UseDeveloperExceptionPage(); - app.UseCors(cors => { + app.UseCors(cors => + { cors.AllowAnyMethod(); cors.AllowAnyHeader(); cors.SetIsOriginAllowed(_ => true); @@ -213,7 +229,8 @@ public static class Program }); } - if (app.Environment.IsProduction()) { + if (app.Environment.IsProduction()) + { app.UseForwardedHeaders(); } @@ -226,7 +243,8 @@ public static class Program .UseAuthentication() .UseAuthorization() .UseSwagger() - .UseSwaggerUI(options => { + .UseSwaggerUI(options => + { options.SwaggerEndpoint(ApiSpecV1.Document.SwaggerPath, ApiSpecV1.Document.VersionName); options.DocumentTitle = AppConstants.API_NAME; }) @@ -234,15 +252,20 @@ public static class Program return app; } - public static int Main(string[] args) { - try { + public static int Main(string[] args) + { + try + { CreateWebApplication(CreateAppBuilder(args)).Run(); return 0; - } catch (Exception ex) { + } + catch (Exception ex) + { Log.Fatal(ex, "Unhandled exception"); return 1; } - finally { + finally + { Log.Information("Shut down complete, flushing logs..."); Log.CloseAndFlush(); } diff --git a/code/api/src/Services/EmailValidationService.cs b/code/api/src/Services/EmailValidationService.cs index e88dfec..c7be20a 100644 --- a/code/api/src/Services/EmailValidationService.cs +++ b/code/api/src/Services/EmailValidationService.cs @@ -1,5 +1,3 @@ -using IOL.GreatOffice.Api.Models.Database; - namespace IOL.GreatOffice.Api.Services; public class EmailValidationService diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs index a179e10..d4aeb0d 100644 --- a/code/api/src/Services/PasswordResetService.cs +++ b/code/api/src/Services/PasswordResetService.cs @@ -1,5 +1,3 @@ -using IOL.GreatOffice.Api.Models.Database; - namespace IOL.GreatOffice.Api.Services; public class PasswordResetService @@ -14,7 +12,8 @@ public class PasswordResetService MainAppDatabase database, VaultService vaultService, ILogger<PasswordResetService> logger, - MailService mailService, IStringLocalizer<SharedResources> localizer) { + MailService mailService, IStringLocalizer<SharedResources> localizer) + { _database = database; _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger; @@ -22,11 +21,13 @@ public class PasswordResetService _localizer = localizer; } - public async Task<PasswordResetRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) { + public async Task<PasswordResetRequest> GetRequestAsync(Guid id, CancellationToken cancellationToken = default) + { var request = await _database.PasswordResetRequests .Include(c => c.User) .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); - if (request == default) { + if (request == default) + { return default; } @@ -34,7 +35,8 @@ public class PasswordResetService return request; } - public async Task<FulfillPasswordResetRequestResult> FulfillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) { + public async Task<FulfillPasswordResetRequestResult> FulfillRequestAsync(Guid id, string newPassword, CancellationToken cancellationToken = default) + { var request = await GetRequestAsync(id, cancellationToken); if (request == default) return FulfillPasswordResetRequestResult.REQUEST_NOT_FOUND; var user = _database.Users.FirstOrDefault(c => c.Id == request.User.Id); @@ -47,13 +49,15 @@ public class PasswordResetService return FulfillPasswordResetRequestResult.FULFILLED; } - public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) { + public async Task AddRequestAsync(User user, TimeZoneInfo requestTz, CancellationToken cancellationToken = default) + { await DeleteRequestsForUserAsync(user.Id, cancellationToken); var request = new PasswordResetRequest(user); _database.PasswordResetRequests.Add(request); await _database.SaveChangesAsync(cancellationToken); var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id); - var message = new MailService.PostmarkEmail() { + var message = new MailService.PostmarkEmail() + { To = request.User.Username, Subject = _localizer["Reset password - Greatoffice"], TextBody = _localizer[""" @@ -68,16 +72,16 @@ If you did not request a password reset, no action is required. """, user.DisplayName(true), _configuration.CANONICAL_FRONTEND_URL, request.Id, zonedExpirationDate.ToString("yyyy-MM-dd hh:mm")] }; -#pragma warning disable 4014 - Task.Run(() => { -#pragma warning restore 4014 - _mailService.SendMailAsync(message); - _logger.LogInformation($"Added password reset request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(AppDateTime.UtcNow)}."); - }, - cancellationToken); + await Task.Run(() => + { + _mailService.SendMailAsync(message).ConfigureAwait(false); + _logger.LogInformation($"Added password reset request for user: {request.User.Username}, expires in {request.ExpirationDate.Subtract(AppDateTime.UtcNow)}."); + }, + cancellationToken).ConfigureAwait(false); } - public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) { + public async Task DeleteRequestsForUserAsync(Guid userId, CancellationToken cancellationToken = default) + { var requestsToRemove = _database.PasswordResetRequests.Where(c => c.UserId == userId).ToList(); if (!requestsToRemove.Any()) return; _database.PasswordResetRequests.RemoveRange(requestsToRemove); @@ -85,10 +89,13 @@ If you did not request a password reset, no action is required. _logger.LogInformation($"Deleted {requestsToRemove.Count} password reset requests for user: {userId}."); } - public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) { + public async Task DeleteStaleRequestsAsync(CancellationToken cancellationToken = default) + { var deleteCount = 0; - foreach (var request in _database.PasswordResetRequests.Where(c => c.IsExpired)) { - if (!request.IsExpired) { + foreach (var request in _database.PasswordResetRequests.Where(c => c.IsExpired)) + { + if (!request.IsExpired) + { continue; } diff --git a/code/api/src/Services/TenantService.cs b/code/api/src/Services/TenantService.cs index 477a865..0de6f53 100644 --- a/code/api/src/Services/TenantService.cs +++ b/code/api/src/Services/TenantService.cs @@ -1,5 +1,3 @@ -using IOL.GreatOffice.Api.Models.Database; - namespace IOL.GreatOffice.Api.Services; public class TenantService diff --git a/code/api/src/Services/UserService.cs b/code/api/src/Services/UserService.cs index 8e183fe..4df8ded 100644 --- a/code/api/src/Services/UserService.cs +++ b/code/api/src/Services/UserService.cs @@ -1,5 +1,3 @@ -using IOL.GreatOffice.Api.Models.Database; - namespace IOL.GreatOffice.Api.Services; public class UserService diff --git a/code/api/src/Utilities/BasicAuthenticationAttribute.cs b/code/api/src/Utilities/BasicAuthenticationAttribute.cs index 0bfd007..9e57595 100644 --- a/code/api/src/Utilities/BasicAuthenticationAttribute.cs +++ b/code/api/src/Utilities/BasicAuthenticationAttribute.cs @@ -5,10 +5,11 @@ namespace IOL.GreatOffice.Api.Utilities; public class BasicAuthenticationAttribute : TypeFilterAttribute { - public BasicAuthenticationAttribute(string claimPermission) : base(typeof(BasicAuthenticationFilter)) { - Arguments = new object[] { + public BasicAuthenticationAttribute(string claimPermission) : base(typeof(BasicAuthenticationFilter)) + { + Arguments = [ new Claim(claimPermission, "True") - }; + ]; } } @@ -16,23 +17,30 @@ public class BasicAuthenticationFilter : IAuthorizationFilter { private readonly Claim _claim; - public BasicAuthenticationFilter(Claim claim) { + public BasicAuthenticationFilter(Claim claim) + { _claim = claim; } - public void OnAuthorization(AuthorizationFilterContext context) { - if (!context.HttpContext.Request.Headers.ContainsKey("Authorization")) return; - try { - var authHeader = AuthenticationHeaderValue.Parse(context.HttpContext.Request.Headers["Authorization"]); - if (authHeader.Parameter is null) { + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues authzHeaderValue)) return; + try + { + var authHeader = AuthenticationHeaderValue.Parse(authzHeaderValue); + if (authHeader.Parameter is null) + { context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME); } var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value); - if (!hasClaim) { + if (!hasClaim) + { context.Result = new ForbidResult(AppConstants.BASIC_AUTH_SCHEME); } - } catch { + } + catch + { // ignore } } diff --git a/code/api/src/Utilities/BasicAuthenticationHandler.cs b/code/api/src/Utilities/BasicAuthenticationHandler.cs index 3b92293..41486ef 100644 --- a/code/api/src/Utilities/BasicAuthenticationHandler.cs +++ b/code/api/src/Utilities/BasicAuthenticationHandler.cs @@ -1,7 +1,6 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Encodings.Web; -using IOL.GreatOffice.Api.Models.Database; using Microsoft.Extensions.Options; namespace IOL.GreatOffice.Api.Utilities; @@ -16,17 +15,18 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, - ISystemClock clock, MainAppDatabase context, VaultService vaultService ) : - base(options, logger, encoder, clock) { + base(options, logger, encoder) + { _context = context; _configuration = vaultService.GetCurrentAppConfiguration(); _logger = logger.CreateLogger<BasicAuthenticationHandler>(); } - protected override Task<AuthenticateResult> HandleAuthenticateAsync() { + protected override Task<AuthenticateResult> HandleAuthenticateAsync() + { var endpoint = Context.GetEndpoint(); if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null) return Task.FromResult(AuthenticateResult.NoResult()); @@ -34,9 +34,11 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc if (!Request.Headers.ContainsKey("Authorization")) return Task.FromResult(AuthenticateResult.Fail("Missing Authorization Header")); - try { + try + { var tokenEntropy = _configuration.APP_AES_KEY; - if (tokenEntropy.IsNullOrWhiteSpace()) { + if (tokenEntropy.IsNullOrWhiteSpace()) + { _logger.LogWarning("No token entropy is available in env:TOKEN_ENTROPY, Basic auth is disabled"); return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); } @@ -47,16 +49,19 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc var decryptedString = Encoding.UTF8.GetString(credentialBytes).DecryptWithAes(tokenEntropy); var tokenIsGuid = Guid.TryParse(decryptedString, out var tokenId); - if (!tokenIsGuid) { + if (!tokenIsGuid) + { return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); } var token = _context.AccessTokens.Include(c => c.User).SingleOrDefault(c => c.Id == tokenId); - if (token == default) { + if (token == default) + { return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Not Found")); } - if (token.HasExpired) { + if (token.HasExpired) + { return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header: Expired")); } @@ -72,7 +77,9 @@ public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSc var ticket = new AuthenticationTicket(principal, AppConstants.BASIC_AUTH_SCHEME); return Task.FromResult(AuthenticateResult.Success(ticket)); - } catch (Exception e) { + } + catch (Exception e) + { _logger.LogError(e, $"An exception occured when challenging {AppConstants.BASIC_AUTH_SCHEME}"); return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header")); } diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs b/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs index 10525fd..71578eb 100644 --- a/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs +++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/ApplicationTests/LoginPageTests.cs @@ -7,12 +7,14 @@ public class LoginPageTests : IClassFixture<WebServerFixture> { private readonly WebServerFixture _fixture; - public LoginPageTests(WebServerFixture fixture) { + public LoginPageTests(WebServerFixture fixture) + { _fixture = fixture; } [Fact] - public async Task LoginPageTestsRenders() { + public async Task LoginPageTestsRenders() + { var page = await _fixture.Browser.NewPageAsync(); await page.GotoAsync(_fixture.BaseUrl); diff --git a/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs index 080fa9f..de316de 100644 --- a/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs +++ b/code/api/tests/IOL.GreatOffice.IntegrationTests/Helpers/WebServerFixture.cs @@ -7,7 +7,6 @@ using Program = IOL.GreatOffice.Api.Program; namespace IOL.GreatOffice.IntegrationTests.Helpers; -// ReSharper disable once ClassNeverInstantiated.Global public class WebServerFixture : IAsyncLifetime, IDisposable { private readonly WebApplication Host; @@ -15,30 +14,35 @@ public class WebServerFixture : IAsyncLifetime, IDisposable public IBrowser Browser { get; private set; } public string BaseUrl { get; } = $"https://localhost:{GetRandomUnusedPort()}"; - public WebServerFixture() { + public WebServerFixture() + { Host = Program.CreateWebApplication(Program.CreateAppBuilder(default)); } - public async Task InitializeAsync() { + public async Task InitializeAsync() + { Playwright = await Microsoft.Playwright.Playwright.CreateAsync(); Browser = await Playwright.Chromium.LaunchAsync(); await Host.StartAsync(); } - public async Task DisposeAsync() { + public async Task DisposeAsync() + { await Host.StopAsync(); await Host.DisposeAsync(); Playwright?.Dispose(); } - public void Dispose() { + public void Dispose() + { Host.StopAsync(); Host.DisposeAsync(); Playwright?.Dispose(); GC.SuppressFinalize(this); } - private static int GetRandomUnusedPort() { + private static int GetRandomUnusedPort() + { var listener = new TcpListener(IPAddress.Any, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; diff --git a/code/app/bun.lockb b/code/app/bun.lockb Binary files differindex 2b9eee0..450c49f 100755 --- a/code/app/bun.lockb +++ b/code/app/bun.lockb diff --git a/code/app/package.json b/code/app/package.json index 53945f3..bd64dba 100644 --- a/code/app/package.json +++ b/code/app/package.json @@ -15,15 +15,9 @@ }, "devDependencies": { "@faker-js/faker": "^7.6.0", - "@playwright/test": "^1.31.1", - "@sveltejs/adapter-node": "1.2.0", "@sveltejs/kit": "1.9.2", "@tailwindcss/forms": "^0.5.3", - "@types/js-cookie": "^3.0.3", - "@vite-pwa/sveltekit": "^0.1.3", - "autoprefixer": "^10.4.13", "npm-run-all": "^4.1.5", - "pino-pretty": "^9.4.0", "postcss": "^8.4.21", "postcss-load-config": "^4.0.1", "svelte": "^3.55.1", @@ -37,14 +31,7 @@ "vite-plugin-pwa": "^0.14.4" }, "dependencies": { - "@developermuch/dev-svelte-headlessui": "0.0.1", - "@rgossiaux/svelte-headlessui": "^1.0.2", - "fuzzysort": "^2.0.4", - "js-cookie": "^3.0.1", - "pino": "^8.11.0", - "pino-seq": "^0.9.0", "svelte-headless-table": "^0.17.2", - "temporal-polyfill": "^0.1.1", - "turbo-query": "^1.9.0" + "temporal-polyfill": "^0.1.1" } -}
\ No newline at end of file +} diff --git a/code/app/src/utilities/_fetch.ts b/code/app/src/utilities/_fetch.ts index 415e1c2..f884653 100644 --- a/code/app/src/utilities/_fetch.ts +++ b/code/app/src/utilities/_fetch.ts @@ -1,28 +1,28 @@ -import {Temporal} from "temporal-polyfill"; -import {redirect} from "@sveltejs/kit"; -import {browser} from "$app/environment"; -import {goto} from "$app/navigation"; -import {SignInPageMessage, signInPageMessageQueryKey} from "$routes/(main)/(public)/sign-in"; -import {AccountService} from "$services/account-service"; +import { Temporal } from "temporal-polyfill"; +import { redirect } from "@sveltejs/kit"; +import { browser } from "$app/environment"; +import { goto } from "$app/navigation"; +import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in"; +import { AccountService } from "$services/account-service"; export async function http_post_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { const init = make_request_init("post", body, abort_signal); - const response = await internal_fetch_async({url, init, timeout}); + const response = await internal_fetch_async({ url, init, timeout }); if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); return response; } export async function http_get_async(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { const init = make_request_init("get", undefined, abort_signal); - const response = await internal_fetch_async({url, init, timeout}); + const response = await internal_fetch_async({ url, init, timeout }); if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); return response; } export async function http_delete_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { const init = make_request_init("delete", body, abort_signal); - const response = await internal_fetch_async({url, init, timeout}); + const response = await internal_fetch_async({ url, init, timeout }); if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); return response; } @@ -42,11 +42,10 @@ async function internal_fetch_async(request: InternalFetchRequest): Promise<Resp response = await fetch(fetch_request); } } catch (error: any) { - console.error(error); if (error.message === "Timeout") { - console.error("Request timed out"); + console.error("Request timed out", error); } else if (error.message === "Network request failed") { - console.error("No internet connection"); + console.error("No internet connection", error); } else { throw error; } |
